From aa850f9a3bece5fa353b45adea9eee59fc7e6996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 6 Jul 2026 18:43:08 +0800 Subject: [PATCH 01/12] feat: input history (#4104) --- .github/pull_request_template.md | 1 + .../{validate-form.yml => validate-issue.yml} | 4 +- .github/workflows/validate-pr.yml | 36 +++++++++++++++++ CHANGELOG.md | 2 + scripts/validate-form/main.js | 32 +++++++++++++-- yazi-actor/src/input/close.rs | 10 ++++- yazi-actor/src/input/mod.rs | 2 +- yazi-actor/src/input/recall.rs | 32 +++++++++++++++ yazi-actor/src/input/remember.rs | 33 +++++++++++++++ yazi-config/preset/keymap-default.toml | 8 ++++ yazi-config/src/popup/input.rs | 8 ++++ yazi-core/src/input/guard.rs | 16 ++++---- yazi-core/src/input/history.rs | 31 ++++++++++++++ yazi-core/src/input/input.rs | 24 ++++++----- yazi-core/src/input/mod.rs | 2 +- yazi-fm/src/executor.rs | 2 + yazi-parser/src/input/mod.rs | 2 +- yazi-parser/src/input/recall.rs | 24 +++++++++++ yazi-parser/src/spark/spark.rs | 6 +++ yazi-widgets/src/input/actor/backspace.rs | 2 +- yazi-widgets/src/input/actor/casefy.rs | 2 +- yazi-widgets/src/input/actor/complete.rs | 2 +- yazi-widgets/src/input/actor/kill.rs | 2 +- yazi-widgets/src/input/actor/mod.rs | 2 +- yazi-widgets/src/input/actor/recall.rs | 40 +++++++++++++++++++ yazi-widgets/src/input/actor/replace.rs | 2 +- yazi-widgets/src/input/actor/type.rs | 2 +- yazi-widgets/src/input/history.rs | 18 +++++++++ yazi-widgets/src/input/input.rs | 11 ++++- yazi-widgets/src/input/mod.rs | 2 +- yazi-widgets/src/input/option.rs | 2 + 31 files changed, 324 insertions(+), 38 deletions(-) rename .github/workflows/{validate-form.yml => validate-issue.yml} (93%) create mode 100644 .github/workflows/validate-pr.yml create mode 100644 yazi-actor/src/input/recall.rs create mode 100644 yazi-actor/src/input/remember.rs create mode 100644 yazi-core/src/input/history.rs create mode 100644 yazi-parser/src/input/recall.rs create mode 100644 yazi-widgets/src/input/actor/recall.rs create mode 100644 yazi-widgets/src/input/history.rs diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8c7716aa..a0e4288f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,3 +19,4 @@ If it has already been detailed in the associated issue, please skip this sectio ## Checklist - [ ] I have read [CONTRIBUTING.md](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md) +- [ ] I confirm this PR follows the [AI Policy](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md#ai-policy) diff --git a/.github/workflows/validate-form.yml b/.github/workflows/validate-issue.yml similarity index 93% rename from .github/workflows/validate-form.yml rename to .github/workflows/validate-issue.yml index 81ccc28a..7e997036 100644 --- a/.github/workflows/validate-form.yml +++ b/.github/workflows/validate-issue.yml @@ -1,4 +1,4 @@ -name: Validate Form +name: Validate Issue on: issues: @@ -24,7 +24,7 @@ jobs: cd scripts/validate-form npm ci - - name: Validate Form + - name: Validate Issue uses: actions/github-script@v9 with: script: | diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 00000000..17d9009e --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,36 @@ +name: Validate PR + +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +permissions: + contents: read + pull-requests: read + +jobs: + check-list: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.repository.default_branch }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + + - name: Install Dependencies + run: | + cd scripts/validate-form + npm ci + + - name: Validate PR + uses: actions/github-script@v9 + with: + script: | + const script = require('./scripts/validate-form/main.js') + await script({github, context, core}) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index eec968e8..0a64090e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Drag and drop ([#4005]) - Bulk create ([#3793]) - Make help menu a command palette ([#4074]) +- Input history ([#4104]) - Make visual mode support wraparound scrolling ([#4101]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - Context-aware icons for inputs ([#4080]) @@ -1771,3 +1772,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4080]: https://github.com/sxyazi/yazi/pull/4080 [#4096]: https://github.com/sxyazi/yazi/pull/4096 [#4101]: https://github.com/sxyazi/yazi/pull/4101 +[#4104]: https://github.com/sxyazi/yazi/pull/4104 diff --git a/scripts/validate-form/main.js b/scripts/validate-form/main.js index bd3fddb8..186d2689 100644 --- a/scripts/validate-form/main.js +++ b/scripts/validate-form/main.js @@ -2,6 +2,15 @@ const LABEL_NAME = "needs info" const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm +const RE_PR_CHECKLIST = /#{2}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm + +function pullRequestBody(content) { + if (RE_PR_CHECKLIST.test(content)) { + return null + } + + return "All required checklist items must be checked in the PR description." +} function bugReportBody(creator, content, hash) { if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) { @@ -41,6 +50,12 @@ Our maintainers work on Yazi in their free time, this helps them work efficientl ` } +function skipValidation(context) { + const login = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login + const owner = context.payload.repository?.owner?.login || context.repo.owner + return !!login && login === owner +} + module.exports = async ({ github, context, core }) => { async function nightlyHash() { try { @@ -214,19 +229,23 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y } async function main() { - const hash = await nightlyHash() - if (!hash) return - if (context.eventName === "schedule") { await closeOldIssues() return } + if (skipValidation(context)) { + return + } + if (context.eventName === "issues") { const id = context.payload.issue.number const content = context.payload.issue.body || "" const creator = context.payload.issue.user.login + const hash = await nightlyHash() + if (!hash) return + if (await hasLabel(id, "bug")) { const body = bugReportBody(creator, content, hash) await updateLabels(id, !!body, body) @@ -236,6 +255,13 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y } else if (context.payload.action === "opened") { await closeUnsupportedIssue(id) } + } else if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + const content = context.payload.pull_request.body || "" + + const body = pullRequestBody(content) + if (body) { + core.setFailed(body) + } } } diff --git a/yazi-actor/src/input/close.rs b/yazi-actor/src/input/close.rs index 19ae12e7..c6ef387f 100644 --- a/yazi-actor/src/input/close.rs +++ b/yazi-actor/src/input/close.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use yazi_core::input::InputMutGuard; use yazi_macro::{act, render, succ}; use yazi_parser::{input::CloseForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; @@ -20,11 +21,16 @@ impl Actor for Close { guard.ticket.next(); if let Some(cb) = guard.cb.take() { - let value = guard.snap().value.clone(); + let value = guard.value().to_owned(); cb(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) }); } - drop(guard); + if form.submit + && let InputMutGuard::Main(input) = guard + { + input.histories.remember(&input.main.history.name, input.main.value()); + } + cx.input.main.visible = false; act!(cmp:close, cx)?; diff --git a/yazi-actor/src/input/mod.rs b/yazi-actor/src/input/mod.rs index 16e44b0e..8f2c1b56 100644 --- a/yazi-actor/src/input/mod.rs +++ b/yazi-actor/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(close complete escape show); +yazi_macro::mod_flat!(close complete escape recall remember show); diff --git a/yazi-actor/src/input/recall.rs b/yazi-actor/src/input/recall.rs new file mode 100644 index 00000000..98a99c2f --- /dev/null +++ b/yazi-actor/src/input/recall.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use yazi_core::input::InputMutGuard; +use yazi_macro::succ; +use yazi_parser::input::RecallForm; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Recall; + +impl Actor for Recall { + type Form = RecallForm; + + const NAME: &str = "recall"; + + fn act(cx: &mut Ctx, form: Self::Form) -> Result { + let Some(input) = cx.input.lock_mut() else { + succ!(); + }; + + match input { + InputMutGuard::Main(input) => { + let entries = input.histories.get(&input.main.history.name); + input.main.recall(entries, form.step) + } + InputMutGuard::Alt(input, mut guard) => { + let entries = input.histories.get(&guard.history.name); + guard.recall(entries, form.step) + } + } + } +} diff --git a/yazi-actor/src/input/remember.rs b/yazi-actor/src/input/remember.rs new file mode 100644 index 00000000..f83039ce --- /dev/null +++ b/yazi-actor/src/input/remember.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use yazi_core::input::InputMutGuard; +use yazi_macro::succ; +use yazi_parser::VoidForm; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Remember; + +impl Actor for Remember { + type Form = VoidForm; + + const NAME: &str = "remember"; + + fn act(cx: &mut Ctx, _: Self::Form) -> Result { + let Some(mut input) = cx.input.lock_mut() else { + succ!(); + }; + + match &mut input { + InputMutGuard::Main(input) => { + input.histories.remember(&input.main.history.name, input.main.value()); + } + InputMutGuard::Alt(input, guard) => { + input.histories.remember(&guard.history.name, guard.value()); + } + } + + input.history.take(); + succ!(); + } +} diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index d39c00e8..db1fa4ff 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -310,6 +310,14 @@ keymap = [ { on = "U", run = "casefy upper", desc = "Uppercase" }, { on = "", run = "redo", desc = "Redo the last operation" }, + # History + { on = "k", run = "recall -1", desc = "Recall previous input" }, + { on = "j", run = "recall 1", desc = "Recall next input" }, + { on = "", run = "recall -1", desc = "Recall previous input" }, + { on = "", run = "recall 1", desc = "Recall next input" }, + { on = "", run = "recall -1", desc = "Recall previous input" }, + { on = "", run = "recall 1", desc = "Recall next input" }, + # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index 8d04c6f8..8abebacf 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -50,6 +50,7 @@ impl Input { name: "cd".to_owned(), title: self.cd_title.clone(), value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, + history: "shared".to_owned(), position: Position::new(self.cd_origin, self.cd_offset), completion: true, ..Default::default() @@ -60,6 +61,7 @@ impl Input { InputOpt { name: format!("create-{}", if dir { "dir" } else { "file" }), title: self.create_title[dir as usize].clone(), + history: "shared".to_owned(), position: Position::new(self.create_origin, self.create_offset), ..Default::default() } @@ -69,6 +71,7 @@ impl Input { InputOpt { name: format!("rename-{}", if is_dir { "dir" } else { "file" }), title: self.rename_title.clone(), + history: "shared".to_owned(), position: Position::new(self.rename_origin, self.rename_offset), ..Default::default() } @@ -78,6 +81,7 @@ impl Input { InputOpt { name: "filter".to_owned(), title: self.filter_title.clone(), + history: "shared".to_owned(), position: Position::new(self.filter_origin, self.filter_offset), realtime: true, ..Default::default() @@ -88,6 +92,7 @@ impl Input { InputOpt { name: "find".to_owned(), title: self.find_title[prev as usize].clone(), + history: "shared".to_owned(), position: Position::new(self.find_origin, self.find_offset), realtime: true, ..Default::default() @@ -98,6 +103,7 @@ impl Input { InputOpt { name: "search".to_owned(), title: self.search_title.replace("{n}", name), + history: "shared".to_owned(), position: Position::new(self.search_origin, self.search_offset), ..Default::default() } @@ -107,6 +113,7 @@ impl Input { InputOpt { name: "shell".to_owned(), title: self.shell_title[block as usize].clone(), + history: "shared".to_owned(), position: Position::new(self.shell_origin, self.shell_offset), ..Default::default() } @@ -116,6 +123,7 @@ impl Input { InputOpt { name: "tab-rename".to_owned(), title: "Rename tab:".to_owned(), + history: "shared".to_owned(), position: Position::new(Origin::TopCenter, Offset { x: 0, y: 2, diff --git a/yazi-core/src/input/guard.rs b/yazi-core/src/input/guard.rs index c0328928..e7998c41 100644 --- a/yazi-core/src/input/guard.rs +++ b/yazi-core/src/input/guard.rs @@ -1,6 +1,8 @@ use std::ops::{Deref, DerefMut}; -use parking_lot::MutexGuard; +use parking_lot::{ArcMutexGuard, MutexGuard, RawMutex}; + +use crate::input::Input; // --- InputGuard pub enum InputGuard<'a> { @@ -21,8 +23,8 @@ impl Deref for InputGuard<'_> { // --- InputMutGuard pub enum InputMutGuard<'a> { - Main(&'a mut yazi_widgets::input::Input), - Alt(MutexGuard<'a, yazi_widgets::input::Input>), + Main(&'a mut Input), + Alt(&'a mut Input, ArcMutexGuard), } impl Deref for InputMutGuard<'_> { @@ -30,8 +32,8 @@ impl Deref for InputMutGuard<'_> { fn deref(&self) -> &Self::Target { match self { - Self::Main(main) => main, - Self::Alt(alt) => alt, + Self::Main(input) => &input.main.inner, + Self::Alt(_, guard) => guard, } } } @@ -39,8 +41,8 @@ impl Deref for InputMutGuard<'_> { impl DerefMut for InputMutGuard<'_> { fn deref_mut(&mut self) -> &mut Self::Target { match self { - Self::Main(main) => main, - Self::Alt(alt) => alt, + Self::Main(input) => &mut input.main.inner, + Self::Alt(_, guard) => guard, } } } diff --git a/yazi-core/src/input/history.rs b/yazi-core/src/input/history.rs new file mode 100644 index 00000000..9bda9297 --- /dev/null +++ b/yazi-core/src/input/history.rs @@ -0,0 +1,31 @@ +use hashbrown::HashMap; + +#[derive(Default)] +pub struct InputHistories { + inner: HashMap>, +} + +impl InputHistories { + pub fn get(&self, group: &str) -> &[String] { + self.inner.get(group).map(Vec::as_slice).unwrap_or(&[]) + } + + pub fn remember(&mut self, group: &str, value: &str) -> bool { + if group.is_empty() || value.is_empty() { + return false; + } + + let entries = self.inner.entry_ref(group).or_default(); + if entries.last().is_some_and(|last| last == value) { + return false; + } + + entries.retain(|entry| entry != value); + entries.push(value.to_owned()); + if entries.len() > 20 { + entries.drain(..entries.len() - 20); + } + + true + } +} diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 425cc277..56e6afc6 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -4,12 +4,13 @@ use parking_lot::Mutex; use ratatui_widgets::block::Padding; use yazi_binding::{elements::Spatial, position::Position}; -use crate::input::{InputGuard, InputMutGuard}; +use crate::input::{InputGuard, InputHistories, InputMutGuard}; #[derive(Default)] pub struct Input { - pub main: InputMain, - pub alt: Option, + pub main: InputMain, + pub alt: Option, + pub histories: InputHistories, } impl Input { @@ -39,9 +40,10 @@ impl Input { pub fn lock_mut(&mut self) -> Option> { if self.main.visible { - Some(InputMutGuard::Main(&mut self.main.inner)) - } else if let Some(alt) = &self.alt { - Some(InputMutGuard::Alt(alt.inner.lock())) + Some(InputMutGuard::Main(self)) + } else if let Some(alt) = &mut self.alt { + let guard = alt.inner.lock_arc(); + Some(InputMutGuard::Alt(self, guard)) } else { None } @@ -51,11 +53,11 @@ impl Input { // --- InputMain #[derive(Default)] pub struct InputMain { - inner: yazi_widgets::input::Input, - pub name: String, - pub title: String, - pub position: Position, - pub visible: bool, + pub(super) inner: yazi_widgets::input::Input, + pub name: String, + pub title: String, + pub position: Position, + pub visible: bool, } impl Deref for InputMain { diff --git a/yazi-core/src/input/mod.rs b/yazi-core/src/input/mod.rs index 60b5cf6d..4aa69d91 100644 --- a/yazi-core/src/input/mod.rs +++ b/yazi-core/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(guard input); +yazi_macro::mod_flat!(guard history input); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 431b97b0..61e06097 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -267,6 +267,8 @@ impl<'a> Executor<'a> { on!(escape); on!(show); on!(close); + on!(recall); + on!(remember); guard = match self.app.core.input.lock_mut() { Some(g) => g, diff --git a/yazi-parser/src/input/mod.rs b/yazi-parser/src/input/mod.rs index 37e61514..ed31961f 100644 --- a/yazi-parser/src/input/mod.rs +++ b/yazi-parser/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(close show); +yazi_macro::mod_flat!(close recall show); diff --git a/yazi-parser/src/input/recall.rs b/yazi-parser/src/input/recall.rs new file mode 100644 index 00000000..5d002fcf --- /dev/null +++ b/yazi-parser/src/input/recall.rs @@ -0,0 +1,24 @@ +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use serde::Deserialize; +use yazi_shared::event::ActionCow; +use yazi_widgets::Step; + +#[derive(Clone, Copy, Debug, Deserialize)] +pub struct RecallForm { + #[serde(alias = "0")] + pub step: Step, +} + +impl TryFrom for RecallForm { + type Error = anyhow::Error; + + fn try_from(a: ActionCow) -> Result { Ok(a.deserialize()?) } +} + +impl FromLua for RecallForm { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for RecallForm { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 46745ac6..4f88962d 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -126,6 +126,8 @@ pub enum Spark<'a> { InputKill(yazi_widgets::input::parser::KillOpt), InputMove(yazi_widgets::input::parser::MoveOpt), InputPaste(yazi_widgets::input::parser::PasteOpt), + InputRecall(crate::input::RecallForm), + InputRemember(crate::VoidForm), InputShow(crate::input::ShowForm), // Notify @@ -319,6 +321,8 @@ impl<'a> IntoLua for Spark<'a> { Self::InputKill(b) => b.into_lua(lua), Self::InputMove(b) => b.into_lua(lua), Self::InputPaste(b) => b.into_lua(lua), + Self::InputRecall(b) => b.into_lua(lua), + Self::InputRemember(b) => b.into_lua(lua), Self::InputShow(b) => b.into_lua(lua), // Notify @@ -376,6 +380,7 @@ try_from_spark!( mgr:suspend, mgr:unyank, mgr:watch, + input:remember, which:dismiss ); @@ -464,3 +469,4 @@ try_from_spark!(yazi_widgets::input::parser::InsertOpt, input:insert); try_from_spark!(yazi_widgets::input::parser::KillOpt, input:kill); try_from_spark!(yazi_widgets::input::parser::MoveOpt, input:move); try_from_spark!(yazi_widgets::input::parser::PasteOpt, input:paste); +try_from_spark!(crate::input::RecallForm, input:recall); diff --git a/yazi-widgets/src/input/actor/backspace.rs b/yazi-widgets/src/input/actor/backspace.rs index 11407702..549cae58 100644 --- a/yazi-widgets/src/input/actor/backspace.rs +++ b/yazi-widgets/src/input/actor/backspace.rs @@ -21,7 +21,7 @@ impl Input { act!(r#move, self, -1)?; } - self.flush_type(); + self.flush_all(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/casefy.rs b/yazi-widgets/src/input/actor/casefy.rs index 3db1ce00..4544a367 100644 --- a/yazi-widgets/src/input/actor/casefy.rs +++ b/yazi-widgets/src/input/actor/casefy.rs @@ -22,7 +22,7 @@ impl Input { snap.value.replace_range(start..end, &casefied); snap.op = InputOp::None; snap.cursor = range.start; - self.snaps.tag(self.size.width as usize).then(|| self.flush_type()); + self.snaps.tag(self.size.width as usize).then(|| self.flush_all()); act!(r#move, self)?; succ!(render!()); diff --git a/yazi-widgets/src/input/actor/complete.rs b/yazi-widgets/src/input/actor/complete.rs index 00884ecb..c54980c4 100644 --- a/yazi-widgets/src/input/actor/complete.rs +++ b/yazi-widgets/src/input/actor/complete.rs @@ -25,7 +25,7 @@ impl Input { snap.value = new; act!(r#move, self, delta)?; - self.flush_type(); + self.flush_all(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/kill.rs b/yazi-widgets/src/input/actor/kill.rs index c2f44931..51969ee5 100644 --- a/yazi-widgets/src/input/actor/kill.rs +++ b/yazi-widgets/src/input/actor/kill.rs @@ -45,7 +45,7 @@ impl Input { } act!(r#move, self)?; - self.flush_type(); + self.flush_all(); succ!(render!()); } diff --git a/yazi-widgets/src/input/actor/mod.rs b/yazi-widgets/src/input/actor/mod.rs index 7913df7d..999e9597 100644 --- a/yazi-widgets/src/input/actor/mod.rs +++ b/yazi-widgets/src/input/actor/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(actor backspace backward casefy complete delete escape forward insert kill paste r#move r#type redo replace undo visual yank); +yazi_macro::mod_flat!(actor backspace backward casefy complete delete escape forward insert kill paste recall r#move r#type redo replace undo visual yank); diff --git a/yazi-widgets/src/input/actor/recall.rs b/yazi-widgets/src/input/actor/recall.rs new file mode 100644 index 00000000..e4f1159a --- /dev/null +++ b/yazi-widgets/src/input/actor/recall.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use yazi_macro::{render, succ}; +use yazi_shared::data::Data; + +use crate::{Step, input::{Input, InputSnap}}; + +impl Input { + pub fn recall(&mut self, items: &[String], step: Step) -> Result { + if items.is_empty() { + succ!(); + } + + let pos = self.history.at.unwrap_or(items.len()); + let next = step.add(pos, items.len() + 1, 0, 0, 0); + + if next == pos { + succ!(); + } else if next == items.len() { + let draft = self.history.take(); + return self.recall_to(draft); + } else if self.history.at.is_none() { + self.history.draft = self.value().to_owned(); + } + + self.history.at = Some(next); + self.recall_to(items[next].clone()) + } + + fn recall_to(&mut self, value: String) -> Result { + let mode = self.mode(); + + let mut snap = InputSnap::new(value, self.obscure); + snap.mode = mode; + snap.resize(self.size.width as usize); + + *self.snap_mut() = snap; + self.flush_type(); + succ!(render!()); + } +} diff --git a/yazi-widgets/src/input/actor/replace.rs b/yazi-widgets/src/input/actor/replace.rs index 39a47d4f..a0ac14d2 100644 --- a/yazi-widgets/src/input/actor/replace.rs +++ b/yazi-widgets/src/input/actor/replace.rs @@ -29,7 +29,7 @@ impl Input { (Some(_), Some((len, _))) => snap.value.replace_range(start..start + len, &s), } - self.snaps.tag(self.size.width as usize).then(|| self.flush_type()); + self.snaps.tag(self.size.width as usize).then(|| self.flush_all()); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/type.rs b/yazi-widgets/src/input/actor/type.rs index 90d1390c..2de0332c 100644 --- a/yazi-widgets/src/input/actor/type.rs +++ b/yazi-widgets/src/input/actor/type.rs @@ -31,7 +31,7 @@ impl Input { } act!(r#move, self, s.chars().count() as isize)?; - self.flush_type(); + self.flush_all(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/history.rs b/yazi-widgets/src/input/history.rs new file mode 100644 index 00000000..f82de7f8 --- /dev/null +++ b/yazi-widgets/src/input/history.rs @@ -0,0 +1,18 @@ +use std::mem; + +#[derive(Debug, Default)] +pub struct InputHistory { + pub name: String, + + pub(super) at: Option, + pub(super) draft: String, +} + +impl InputHistory { + pub(super) fn new(name: String) -> Self { Self { name, ..Default::default() } } + + pub fn take(&mut self) -> String { + self.at = None; + mem::take(&mut self.draft) + } +} diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index 62d47ead..cf54f7d9 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -7,13 +7,14 @@ use yazi_shared::id::Ids; use yazi_shim::path::CROSS_SEPARATOR; use yazi_tty::sequence::SetCursorStyle; -use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp}; +use super::{InputHistory, InputSnap, InputSnaps, mode::InputMode, op::InputOp}; use crate::{CLIPBOARD, input::{InputCallback, InputEvent, InputOpt, InputStyles}}; #[derive(Debug, Default)] pub struct Input { pub size: Size, pub snaps: InputSnaps, + pub history: InputHistory, pub styles: InputStyles, pub obscure: bool, pub realtime: bool, @@ -27,6 +28,7 @@ impl Input { pub fn new(opt: InputOpt) -> Result { let mut input = Self { snaps: InputSnaps::new(opt.value, opt.obscure), + history: InputHistory::new(opt.history), styles: opt.styles, obscure: opt.obscure, realtime: opt.realtime, @@ -89,11 +91,16 @@ impl Input { return false; } if !matches!(old.op, InputOp::None | InputOp::Select(_)) { - self.snaps.tag(self.size.width as usize).then(|| self.flush_type()); + self.snaps.tag(self.size.width as usize).then(|| self.flush_all()); } true } + pub(super) fn flush_all(&mut self) { + self.history.take(); + self.flush_type(); + } + pub(super) fn flush_type(&mut self) { self.ticket.next(); if let Some(cb) = self.cb.as_ref().filter(|_| self.realtime) { diff --git a/yazi-widgets/src/input/mod.rs b/yazi-widgets/src/input/mod.rs index 57b06015..f37955fc 100644 --- a/yazi-widgets/src/input/mod.rs +++ b/yazi-widgets/src/input/mod.rs @@ -1,3 +1,3 @@ yazi_macro::mod_pub!(actor parser); -yazi_macro::mod_flat!(callback chars event gait input input_arc mode op option snap snaps stream styles widget); +yazi_macro::mod_flat!(callback chars event gait history input input_arc mode op option snap snaps stream styles widget); diff --git a/yazi-widgets/src/input/option.rs b/yazi-widgets/src/input/option.rs index d3d50c65..3bcc3648 100644 --- a/yazi-widgets/src/input/option.rs +++ b/yazi-widgets/src/input/option.rs @@ -9,6 +9,7 @@ pub struct InputOpt { pub name: String, pub title: String, pub value: String, + pub history: String, pub styles: InputStyles, pub cursor: Option, pub obscure: bool, @@ -46,6 +47,7 @@ impl TryFrom<&Table> for InputOpt { name: t.raw_get("name").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(), + history: t.raw_get("history").unwrap_or_default(), styles: t.raw_get("styles")?, cursor: None, obscure: t.raw_get("obscure")?, From dbb0cc0d556d73935ebdc1e3d61a2a8aa27059d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 8 Jul 2026 17:04:12 +0800 Subject: [PATCH 02/12] feat: experimental `%y`, `%Y`, `%t`, `%T`, `%yN`, `%YN`, `%tN`, `%TN` shell formatting parameters (#4108) --- CHANGELOG.md | 2 + Cargo.lock | 171 +++++++++++--------- Cargo.toml | 5 +- flake.lock | 12 +- yazi-actor/src/app/reflow.rs | 4 +- yazi-actor/src/context.rs | 3 + yazi-actor/src/lives/core.rs | 4 +- yazi-actor/src/mgr/bulk_create.rs | 17 +- yazi-actor/src/mgr/bulk_rename.rs | 15 +- yazi-actor/src/mgr/copy.rs | 2 +- yazi-actor/src/mgr/hover.rs | 2 +- yazi-actor/src/mgr/open.rs | 2 +- yazi-actor/src/mgr/open_do.rs | 50 +++--- yazi-actor/src/mgr/shell.rs | 12 +- yazi-actor/src/mgr/update_peeked.rs | 2 +- yazi-actor/src/mgr/update_spotted.rs | 2 +- yazi-actor/src/tasks/mod.rs | 2 +- yazi-actor/src/tasks/open_shell_compat.rs | 19 --- yazi-binding/src/chan.rs | 20 +-- yazi-binding/src/composer.rs | 6 +- yazi-binding/src/elements/color.rs | 4 +- yazi-binding/src/elements/constraint.rs | 4 +- yazi-binding/src/elements/line.rs | 4 +- yazi-binding/src/elements/text.rs | 4 +- yazi-binding/src/error.rs | 6 +- yazi-binding/src/permit.rs | 2 +- yazi-binding/src/position/lua.rs | 4 +- yazi-binding/src/process/child.rs | 8 +- yazi-binding/src/process/command.rs | 21 ++- yazi-codegen/src/lib.rs | 12 +- yazi-config/src/keymap/key.rs | 13 +- yazi-config/src/opener/opener.rs | 31 ++-- yazi-config/src/opener/opener_rule.rs | 12 +- yazi-config/src/theme/custom_section_arc.rs | 4 +- yazi-core/src/help/help.rs | 2 +- yazi-core/src/mgr/mgr.rs | 34 +--- yazi-core/src/mgr/mod.rs | 2 +- yazi-core/src/mgr/snap.rs | 54 +++++++ yazi-core/src/spot/lock.rs | 8 +- yazi-core/src/tab/folder.rs | 3 + yazi-core/src/tab/mod.rs | 2 +- yazi-core/src/tab/preview_lock.rs | 12 +- yazi-core/src/tab/snap.rs | 17 ++ yazi-core/src/tab/tab.rs | 5 +- yazi-core/src/tasks/mod.rs | 2 +- yazi-core/src/tasks/process.rs | 37 ----- yazi-fm/Cargo.toml | 2 +- yazi-fm/src/executor.rs | 1 - yazi-fm/src/router.rs | 4 +- yazi-fs/src/splatter.rs | 120 ++++++-------- yazi-parser/src/tasks/process_open.rs | 4 +- yazi-plugin/src/fs/fs.rs | 14 +- yazi-plugin/src/fs/op.rs | 4 +- yazi-plugin/src/pubsub/pubsub.rs | 14 +- yazi-plugin/src/tasks/option.rs | 4 +- yazi-plugin/src/ui/utils.rs | 12 +- yazi-plugin/src/utils/app.rs | 4 +- yazi-plugin/src/utils/json.rs | 4 +- yazi-plugin/src/utils/layer.rs | 4 +- yazi-plugin/src/utils/log.rs | 26 +-- yazi-plugin/src/utils/sync.rs | 6 +- yazi-plugin/src/utils/tasks.rs | 4 +- yazi-plugin/src/utils/text.rs | 10 +- yazi-proxy/src/tasks.rs | 25 +-- yazi-runner/src/loader/chunk.rs | 2 +- yazi-runner/src/loader/loader.rs | 2 +- yazi-runner/src/loader/require.rs | 8 +- yazi-scheduler/src/process/in.rs | 23 ++- yazi-scheduler/src/process/mod.rs | 2 +- yazi-scheduler/src/process/option.rs | 18 --- yazi-scheduler/src/process/process.rs | 10 +- yazi-scheduler/src/process/shell.rs | 34 ++-- yazi-scheduler/src/scheduler.rs | 18 +-- yazi-shared/Cargo.toml | 2 +- yazi-shared/src/any_data.rs | 4 +- yazi-shared/src/id/lua.rs | 12 +- yazi-shared/src/layer.rs | 4 +- yazi-shared/src/path/lua.rs | 14 +- yazi-shared/src/url/lua.rs | 16 +- yazi-shim/src/mlua/ser.rs | 2 +- yazi-shim/src/mlua/string.rs | 8 +- yazi-term/Cargo.toml | 1 + yazi-term/src/event/keyboard.rs | 27 ++-- yazi-term/src/parser/csi.rs | 27 +++- yazi-tty/src/sequence/query.rs | 7 - yazi-tui/src/raterm.rs | 22 +-- yazi-tui/src/state.rs | 13 +- yazi-vfs/src/provider/lua.rs | 4 +- yazi-widgets/src/input/actor/type.rs | 9 +- 89 files changed, 561 insertions(+), 649 deletions(-) delete mode 100644 yazi-actor/src/tasks/open_shell_compat.rs create mode 100644 yazi-core/src/mgr/snap.rs create mode 100644 yazi-core/src/tab/snap.rs delete mode 100644 yazi-core/src/tasks/process.rs delete mode 100644 yazi-scheduler/src/process/option.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a64090e..15e56815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Bulk create ([#3793]) - Make help menu a command palette ([#4074]) - Input history ([#4104]) +- Experimental `%y`, `%Y`, `%t`, `%T`, `%yN`, `%YN`, `%tN`, `%TN` shell formatting parameters ([#4108]) - Make visual mode support wraparound scrolling ([#4101]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - Context-aware icons for inputs ([#4080]) @@ -1773,3 +1774,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4096]: https://github.com/sxyazi/yazi/pull/4096 [#4101]: https://github.com/sxyazi/yazi/pull/4101 [#4104]: https://github.com/sxyazi/yazi/pull/4104 +[#4108]: https://github.com/sxyazi/yazi/pull/4108 diff --git a/Cargo.lock b/Cargo.lock index cec37e11..dba80645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,9 +543,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -780,18 +780,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -799,18 +799,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -1194,14 +1194,16 @@ dependencies = [ [[package]] name = "exr" -version = "1.74.0" +version = "1.74.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +checksum = "6be87932f10230a4339ab394edd8e4611fcb72553d8295b4d52ea55249b3daa5" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", + "num-complex", + "pulp", "smallvec", "zune-inflate", ] @@ -1695,9 +1697,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1" +checksum = "dd854a95a4ac672fed8c054136039fd32c22cf039ff09ead7280afe920486483" dependencies = [ "bitflags", "inotify-sys", @@ -1706,9 +1708,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" dependencies = [ "libc", ] @@ -1791,11 +1793,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] @@ -1954,18 +1956,18 @@ dependencies = [ [[package]] name = "lua-src" -version = "550.0.0" +version = "550.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e836dc8ae16806c9bdcf42003a88da27d163433e3f9684c52f0301258004a4fb" +checksum = "75c110c2fa33f34e0de05448e1f3eb2e0631e7a69e2d8ae1586cffc9fc9f9949" dependencies = [ "cc", ] [[package]] name = "luajit-src" -version = "210.6.6+707c12b" +version = "210.7.2+b925b3e" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295" +checksum = "920cf654b23d217c550ceea57c32cd2a413ea27b6d47ed77b5ee0cf655adefa6" dependencies = [ "cc", "which", @@ -1997,9 +1999,9 @@ checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -2048,31 +2050,31 @@ dependencies = [ [[package]] name = "mlua" -version = "0.11.6" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd36acfa49ce6ee56d1307a061dd302c564eee757e6e4cd67eb4f7204846fab" +checksum = "ad72ffa037cf5970c9860674f32f703fda25d86cf217475fe7a79c5f9961bcaa" dependencies = [ "anyhow", "bstr", "either", "erased-serde", "futures-util", + "inventory", "libc", "mlua-sys", "mlua_derive", "num-traits", "parking_lot", "rustc-hash", - "rustversion", "serde", "serde-value", ] [[package]] name = "mlua-sys" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1c3a7fc7580227ece249fd90aa2fa3b39eb2b49d3aec5e103b3e85f2c3dfc8" +checksum = "92136787b906d4e55cfe96cd6c62e010bb1a56889d0d6cf83eb016dbad07576b" dependencies = [ "cc", "cfg-if", @@ -2084,16 +2086,12 @@ dependencies = [ [[package]] name = "mlua_derive" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465bddde514c4eb3b50b543250e97c1d4b284fa3ef7dc0ba2992c77545dbceb2" +checksum = "0232231813b214d44b57c7678e7dbbcfaac35bd1a01acef0dad7c5dfdc1b1973" dependencies = [ - "itertools", - "once_cell", - "proc-macro-error2", "proc-macro2", "quote", - "regex", "syn", ] @@ -2206,6 +2204,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2694,28 +2702,6 @@ dependencies = [ "wnaf", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2745,10 +2731,33 @@ dependencies = [ ] [[package]] -name = "pxfm" -version = "0.1.29" +name = "pulp" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "qoi" @@ -2973,6 +2982,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "rayon" version = "1.12.0" @@ -2993,6 +3011,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3113,9 +3137,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d2039c3e50abb1ec3bdadea27098679da12a33befa9ab4e62adad9a13ebcb2" +checksum = "ca6c3c1dd0a9ad3a9915a2f6e907d158f9a7e9ac32ddc772b003faafc027d9a8" dependencies = [ "aes", "bitflags", @@ -3242,9 +3266,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -3813,9 +3837,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0" dependencies = [ "cc", "libc", @@ -3823,9 +3847,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd" dependencies = [ "libc", "tikv-jemalloc-sys", @@ -5287,6 +5311,7 @@ dependencies = [ "anyhow", "base64", "bitflags", + "compact_str", "futures", "mlua", "parking_lot", @@ -5421,18 +5446,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2778deef..a7a57091 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ base64 = "0.22.1" bitflags = { version = "2.13.0", features = [ "serde" ] } chrono = "0.4.45" clap = { version = "4.6.1", features = [ "derive" ] } +compact_str = { version = "0.9.1", features = [ "serde" ] } core-foundation-sys = "0.8.7" dirs = "6.0.0" dyn-clone = "1.0.20" @@ -55,7 +56,7 @@ indexmap = { version = "2.14.0", features = [ "serde" ] } inventory = "0.3.24" libc = "0.2.186" lru = "0.18.0" -mlua = { version = "0.11.6", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] } +mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] } objc2 = "0.6.4" ordered-float = { version = "5.3.0", features = [ "serde" ] } parking_lot = "0.12.5" @@ -65,7 +66,7 @@ rand = { version = "0.10.2", default-features = false, features = ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] } ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] } regex = "1.12.4" -russh = { version = "0.62.1", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.150" diff --git a/flake.lock b/flake.lock index 052f6d36..f1c51f5c 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1779877693, - "narHash": "sha256-NOF9NAREhxr50bbBfVcVOq+ArCMSoe8dP79Pk2uyARk=", + "lastModified": 1783279667, + "narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4100e830e085863741bc69b156ec4ccd53ab5be0", + "rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1779851998, - "narHash": "sha256-UkkMh3bX9QW4Luqkm98nUaOqKWrU6i65mUnph3WeSSw=", + "lastModified": 1783320166, + "narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6cddd512fa2bf7231f098d3a2f92f6e4cff71e0a", + "rev": "20ee15370c9256669d66968b89ee20a4b0a4e673", "type": "github" }, "original": { diff --git a/yazi-actor/src/app/reflow.rs b/yazi-actor/src/app/reflow.rs index cfb55e3b..9b911c0d 100644 --- a/yazi-actor/src/app/reflow.rs +++ b/yazi-actor/src/app/reflow.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use mlua::Value; +use mlua::{LuaString, Value}; use ratatui_core::layout::Position; use tracing::error; use yazi_actor::lives::Lives; @@ -30,7 +30,7 @@ impl Actor for Reflow { continue; }; - let id: mlua::String = t.get("_id")?; + let id: LuaString = t.get("_id")?; match &*id.as_bytes() { b"current" => layout.current = *t.raw_get::("_area")?, b"preview" => layout.preview = *t.raw_get::("_area")?, diff --git a/yazi-actor/src/context.rs b/yazi-actor/src/context.rs index fbdc2863..16d92db7 100644 --- a/yazi-actor/src/context.rs +++ b/yazi-actor/src/context.rs @@ -108,6 +108,9 @@ impl<'a> Ctx<'a> { #[inline] pub fn hovered(&self) -> Option<&File> { self.tab().hovered() } + #[inline] + pub fn hovered_url(&self) -> Option<&UrlBuf> { self.tab().hovered_url() } + #[inline] pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() } diff --git a/yazi-actor/src/lives/core.rs b/yazi-actor/src/lives/core.rs index 666a219b..92e07589 100644 --- a/yazi-actor/src/lives/core.rs +++ b/yazi-actor/src/lives/core.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataMethods, UserDataRef, Value}; +use mlua::{AnyUserData, IntoLua, LuaString, MetaMethod, UserData, UserDataMethods, UserDataRef, Value}; use paste::paste; use super::{Lives, PtrCell}; @@ -43,7 +43,7 @@ impl Core { impl UserData for Core { fn add_methods>(methods: &mut M) { - methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| { + methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: LuaString| { macro_rules! reuse { ($key:ident, $value:expr) => { match paste! { &me.[] } { diff --git a/yazi-actor/src/mgr/bulk_create.rs b/yazi-actor/src/mgr/bulk_create.rs index e96f59c3..ba294eb3 100644 --- a/yazi-actor/src/mgr/bulk_create.rs +++ b/yazi-actor/src/mgr/bulk_create.rs @@ -8,8 +8,8 @@ use yazi_fs::{FilesOp, Splatter, file::File, provider::{Provider, local::Local}} use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; -use yazi_scheduler::{AppProxy, NotifyProxy}; -use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; +use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_shim::path::CROSS_SEPARATOR; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; @@ -40,13 +40,12 @@ impl Actor for BulkCreate { }); } - TasksProxy::process_exec( - cwd.clone(), - Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), - vec![UrlCow::default(), UrlBuf::from(&tmp).into()], - opener.block, - opener.orphan, - ) + TasksProxy::process_exec(ShellOpt { + cwd: cwd.clone(), + cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run), + block: opener.block, + orphan: opener.orphan, + }) .await; let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume()); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index ba001880..4f42978a 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -11,8 +11,8 @@ use yazi_fs::{FilesOp, Splatter, file::File, max_common_root, path::skip_url, pr use yazi_macro::{err, succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; -use yazi_scheduler::{AppProxy, NotifyProxy}; -use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; +use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; use yazi_vfs::{VfsFile, maybe_exists, provider}; @@ -63,13 +63,12 @@ impl Actor for BulkRename { } batcher.prime(&tmp); - TasksProxy::process_exec( + TasksProxy::process_exec(ShellOpt { cwd, - Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), - vec![UrlCow::default(), UrlBuf::from(&tmp).into()], - opener.block, - opener.orphan, - ) + cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run), + block: opener.block, + orphan: opener.orphan, + }) .await; let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume()); diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 8361da5a..90e5beb2 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -18,7 +18,7 @@ impl Actor for Copy { let mut s = Vec::::new(); let mut it = if form.hovered { - Box::new(cx.hovered().map(|h| &h.url).into_iter()) + Box::new(cx.hovered_url().into_iter()) } else { cx.tab().selected_or_hovered_urls() } diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index ecfab58f..b702d837 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -36,7 +36,7 @@ impl Actor for Hover { // Publish through DDS let tab = tab!(cx); - err!(Pubsub::pub_after_hover(tab.id, tab.hovered().map(|h| &h.url))); + err!(Pubsub::pub_after_hover(tab.id, tab.hovered_url())); succ!(); } } diff --git a/yazi-actor/src/mgr/open.rs b/yazi-actor/src/mgr/open.rs index 03daa337..492abb61 100644 --- a/yazi-actor/src/mgr/open.rs +++ b/yazi-actor/src/mgr/open.rs @@ -24,7 +24,7 @@ impl Actor for Open { succ!(if !opt.targets.is_empty() { Quit::with_selected(opt.targets) } else if opt.hovered { - Quit::with_selected(cx.hovered().map(|h| &h.url)) + Quit::with_selected(cx.hovered_url()) } else { act!(mgr:escape_visual, cx)?; Quit::with_selected(cx.tab().selected_or_hovered_urls()) diff --git a/yazi-actor/src/mgr/open_do.rs b/yazi-actor/src/mgr/open_do.rs index 253151fb..db6232bd 100644 --- a/yazi-actor/src/mgr/open_do.rs +++ b/yazi-actor/src/mgr/open_do.rs @@ -1,13 +1,13 @@ use anyhow::Result; use hashbrown::HashMap; use indexmap::IndexSet; -use yazi_config::YAZI; -use yazi_fs::file::File; +use yazi_config::{YAZI, opener::OpenerRule}; +use yazi_fs::{Splatter, file::File}; use yazi_macro::succ; use yazi_parser::mgr::OpenDoForm; use yazi_proxy::{PickProxy, TasksProxy}; -use yazi_scheduler::process::ProcessOpt; -use yazi_shared::{data::Data, url::{UrlBuf, UrlCow}}; +use yazi_scheduler::process::ShellOpt; +use yazi_shared::{data::Data, url::UrlBuf}; use crate::{Actor, Ctx}; @@ -32,7 +32,7 @@ impl Actor for OpenDo { if targets.is_empty() { succ!(); } else if !opt.interactive { - succ!(Self::match_and_open(cx, opt.cwd, targets)); + succ!(Self::match_and_open(opt.cwd, targets)); } let openers: IndexSet<_> = @@ -42,20 +42,10 @@ impl Actor for OpenDo { } let pick = PickProxy::show(YAZI.pick.open(openers.iter().map(|o| o.desc()).collect())); - let urls: Vec<_> = [UrlCow::default()] - .into_iter() - .chain(targets.into_iter().map(|(file, _)| file.url.into())) - .collect(); + let urls: Vec<_> = targets.into_iter().map(|(file, _)| file.url).collect(); tokio::spawn(async move { if let Some(choice) = pick.await { - TasksProxy::open_shell_compat(ProcessOpt { - cwd: opt.cwd, - cmd: openers[choice].run.clone().into(), - args: urls, - block: openers[choice].block, - orphan: openers[choice].orphan, - spread: openers[choice].spread, - }); + Self::open_with(&openers[choice], &opt.cwd, &urls); } }); succ!(); @@ -63,24 +53,28 @@ impl Actor for OpenDo { } impl OpenDo { - // TODO: remove - fn match_and_open(cx: &Ctx, cwd: UrlBuf, targets: Vec<(File, &str)>) { - let mut openers = HashMap::new(); + fn match_and_open(cwd: UrlBuf, targets: Vec<(File, &str)>) { + let mut openers: HashMap<_, Vec<_>> = Default::default(); for (file, mime) in targets { if let Some(open) = YAZI.open.matches(&file, mime) && let Some(opener) = YAZI.opener.first(&open) { - openers.entry(opener).or_insert_with(|| vec![UrlCow::default()]).push(file.url.into()); + openers.entry(opener).or_default().push(file.url); } } - for (opener, args) in openers { - cx.tasks.open_shell_compat(ProcessOpt { - cwd: cwd.clone(), - cmd: opener.run.clone().into(), - args, - block: opener.block, + for (opener, urls) in openers { + Self::open_with(&opener, &cwd, &urls); + } + } + + fn open_with(opener: &OpenerRule, cwd: &UrlBuf, urls: &[UrlBuf]) { + let size = if opener.spread { urls.len().max(1) } else { 1 }; + for urls in urls.chunks(size) { + TasksProxy::process_open(ShellOpt { + cwd: cwd.clone(), + cmd: Splatter::new(urls).splat(&opener.run), + block: opener.block, orphan: opener.orphan, - spread: opener.spread, }); } } diff --git a/yazi-actor/src/mgr/shell.rs b/yazi-actor/src/mgr/shell.rs index 33fb79c3..96431385 100644 --- a/yazi-actor/src/mgr/shell.rs +++ b/yazi-actor/src/mgr/shell.rs @@ -2,10 +2,12 @@ use std::borrow::Cow; use anyhow::Result; use yazi_config::YAZI; +use yazi_core::mgr::MgrSnap; +use yazi_fs::Splatter; use yazi_macro::{act, input, succ}; use yazi_parser::mgr::ShellForm; use yazi_proxy::TasksProxy; -use yazi_scheduler::process::ProcessOpt; +use yazi_scheduler::process::ShellOpt; use yazi_shared::data::Data; use yazi_widgets::input::InputEvent; @@ -22,7 +24,7 @@ impl Actor for Shell { act!(mgr:escape_visual, cx)?; let cwd = form.cwd.take().unwrap_or_else(|| cx.cwd().clone()); - let selected: Vec<_> = cx.tab().hovered_and_selected().cloned().map(Into::into).collect(); + let snap = MgrSnap::from(&cx.mgr); let input = if form.interactive { Some(input!( @@ -44,13 +46,11 @@ impl Actor for Shell { return; } - TasksProxy::open_shell_compat(ProcessOpt { + TasksProxy::process_open(ShellOpt { cwd, - cmd: form.run.to_string().into(), - args: selected, + cmd: Splatter::new(snap).splat(&*form.run), block: form.block, orphan: form.orphan, - spread: true, }); }); diff --git a/yazi-actor/src/mgr/update_peeked.rs b/yazi-actor/src/mgr/update_peeked.rs index 150ae25e..c93a8964 100644 --- a/yazi-actor/src/mgr/update_peeked.rs +++ b/yazi-actor/src/mgr/update_peeked.rs @@ -13,7 +13,7 @@ impl Actor for UpdatePeeked { const NAME: &str = "update_peeked"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - let Some(hovered) = cx.hovered().map(|h| &h.url) else { + let Some(hovered) = cx.hovered_url() else { succ!(cx.tab_mut().preview.reset()); }; diff --git a/yazi-actor/src/mgr/update_spotted.rs b/yazi-actor/src/mgr/update_spotted.rs index 4f0f23ad..0bf5f955 100644 --- a/yazi-actor/src/mgr/update_spotted.rs +++ b/yazi-actor/src/mgr/update_spotted.rs @@ -14,7 +14,7 @@ impl Actor for UpdateSpotted { fn act(cx: &mut Ctx, mut form: Self::Form) -> Result { let tab = cx.tab_mut(); - let Some(hovered) = tab.hovered().map(|h| &h.url) else { + let Some(hovered) = tab.hovered_url() else { succ!(tab.spot.reset()); }; diff --git a/yazi-actor/src/tasks/mod.rs b/yazi-actor/src/tasks/mod.rs index 542c41d2..980f8dbe 100644 --- a/yazi-actor/src/tasks/mod.rs +++ b/yazi-actor/src/tasks/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(arrow cancel close inspect open_shell_compat process_open show spawn update_succeed); +yazi_macro::mod_flat!(arrow cancel close inspect process_open show spawn update_succeed); diff --git a/yazi-actor/src/tasks/open_shell_compat.rs b/yazi-actor/src/tasks/open_shell_compat.rs deleted file mode 100644 index ff1fb3c1..00000000 --- a/yazi-actor/src/tasks/open_shell_compat.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anyhow::Result; -use yazi_macro::succ; -use yazi_parser::tasks::ProcessOpenForm; -use yazi_shared::data::Data; - -use crate::{Actor, Ctx}; - -pub struct OpenShellCompat; - -// TODO: remove -impl Actor for OpenShellCompat { - type Form = ProcessOpenForm; - - const NAME: &str = "open_shell_compat"; - - fn act(cx: &mut Ctx, Self::Form { opt, .. }: Self::Form) -> Result { - succ!(cx.tasks.open_shell_compat(opt)); - } -} diff --git a/yazi-binding/src/chan.rs b/yazi-binding/src/chan.rs index afea9dc8..651f0bc2 100644 --- a/yazi-binding/src/chan.rs +++ b/yazi-binding/src/chan.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, UserData, UserDataMethods, Value}; +use mlua::{FromLua, IntoLua, IntoLuaMulti, UserData, UserDataMethods, Value}; use yazi_codegen::FromLuaOwned; use crate::Error; @@ -54,16 +54,13 @@ impl UserData for MpscUnboundedRx { } #[derive(FromLuaOwned)] -pub struct OneshotTx(pub Option>); -pub struct OneshotRx(pub Option>); +pub struct OneshotTx(pub tokio::sync::oneshot::Sender); +pub struct OneshotRx(pub tokio::sync::oneshot::Receiver); impl UserData for OneshotTx { fn add_methods>(methods: &mut M) { - methods.add_method_mut("send", |lua, me, value: Value| { - let Some(tx) = me.0.take() else { - return Err("Oneshot sender already used".into_lua_err()); - }; - match tx.send(T::from_lua(value, lua)?) { + methods.add_method_once("send", |lua, me, value: Value| { + match me.0.send(T::from_lua(value, lua)?) { Ok(()) => true.into_lua_multi(lua), Err(_) => (false, Error::custom("Oneshot receiver closed")).into_lua_multi(lua), } @@ -73,11 +70,8 @@ impl UserData for OneshotTx { impl UserData for OneshotRx { fn add_methods>(methods: &mut M) { - methods.add_async_method_mut("recv", |lua, mut me, ()| async move { - let Some(rx) = me.0.take() else { - return Err("Oneshot receiver already used".into_lua_err()); - }; - match rx.await { + methods.add_async_method_once("recv", |lua, me, ()| async move { + match me.0.await { Ok(value) => value.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), } diff --git a/yazi-binding/src/composer.rs b/yazi-binding/src/composer.rs index e12d237f..4a2e5cc8 100644 --- a/yazi-binding/src/composer.rs +++ b/yazi-binding/src/composer.rs @@ -1,5 +1,5 @@ use hashbrown::HashMap; -use mlua::{Lua, MetaMethod, UserData, UserDataMethods, Value}; +use mlua::{Lua, LuaString, MetaMethod, UserData, UserDataMethods, Value}; pub type ComposerGet = fn(&Lua, &[u8]) -> mlua::Result; pub type ComposerSet = fn(&Lua, &[u8], Value) -> mlua::Result; @@ -24,7 +24,7 @@ where S: Fn(&Lua, &[u8], Value) -> mlua::Result + 'static, { fn add_methods>(methods: &mut M) { - methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| { + methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: LuaString| { let key = key.as_bytes(); if let Some(v) = me.cache.get(key.as_ref()) { return Ok(v.clone()); @@ -38,7 +38,7 @@ where methods.add_meta_method_mut( MetaMethod::NewIndex, - |lua, me, (key, value): (mlua::String, Value)| { + |lua, me, (key, value): (LuaString, Value)| { let key = key.as_bytes(); let value = (me.set)(lua, key.as_ref(), value)?; diff --git a/yazi-binding/src/elements/color.rs b/yazi-binding/src/elements/color.rs index a7bf7926..f4900eb5 100644 --- a/yazi-binding/src/elements/color.rs +++ b/yazi-binding/src/elements/color.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value}; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, UserData)] pub struct Color(pub ratatui_core::style::Color); impl Color { @@ -32,5 +32,3 @@ impl FromLua for Color { })) } } - -impl UserData for Color {} diff --git a/yazi-binding/src/elements/constraint.rs b/yazi-binding/src/elements/constraint.rs index 82fe300c..93d5681f 100644 --- a/yazi-binding/src/elements/constraint.rs +++ b/yazi-binding/src/elements/constraint.rs @@ -1,6 +1,6 @@ use mlua::{FromLua, IntoLua, Lua, UserData, Value}; -#[derive(Clone, Copy, Default, FromLua)] +#[derive(Clone, Copy, Default, FromLua, UserData)] pub struct Constraint(pub(super) ratatui_core::layout::Constraint); impl Constraint { @@ -23,5 +23,3 @@ impl Constraint { impl From for ratatui_core::layout::Constraint { fn from(value: Constraint) -> Self { value.0 } } - -impl UserData for Constraint {} diff --git a/yazi-binding/src/elements/line.rs b/yazi-binding/src/elements/line.rs index 4bcc6048..9a19fbea 100644 --- a/yazi-binding/src/elements/line.rs +++ b/yazi-binding/src/elements/line.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, mem, ops::{Deref, DerefMut}}; use ansi_to_tui::IntoText; -use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Function, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Function, IntoLua, Lua, LuaString, MetaMethod, Table, UserData, UserDataMethods, Value}; use ratatui_core::widgets::Widget; use unicode_width::UnicodeWidthChar; @@ -31,7 +31,7 @@ impl Line { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, line): (Table, Self)| Ok(line))?; - let parse = lua.create_function(|_, code: mlua::String| { + let parse = lua.create_function(|_, code: LuaString| { let code = code.as_bytes(); let Some(line) = code.split_inclusive(|&b| b == b'\n').next() else { return Ok(Self::default()); diff --git a/yazi-binding/src/elements/text.rs b/yazi-binding/src/elements/text.rs index fa511d2d..a647d032 100644 --- a/yazi-binding/src/elements/text.rs +++ b/yazi-binding/src/elements/text.rs @@ -1,7 +1,7 @@ use std::{any::TypeId, mem}; use ansi_to_tui::IntoText; -use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, LuaString, MetaMethod, Table, UserData, UserDataMethods, Value}; use ratatui_core::widgets::Widget; use yazi_shim::SStr; @@ -24,7 +24,7 @@ impl Text { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, text): (Table, Self)| Ok(text))?; - let parse = lua.create_function(|_, code: mlua::String| { + let parse = lua.create_function(|_, code: LuaString| { Ok(Self { inner: code.as_bytes().into_text().into_lua_err()?, ..Default::default() }) })?; diff --git a/yazi-binding/src/error.rs b/yazi-binding/src/error.rs index ddf5e52b..b13103f6 100644 --- a/yazi-binding/src/error.rs +++ b/yazi-binding/src/error.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, fmt::Display}; -use mlua::{ExternalError, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; +use mlua::{ExternalError, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; use yazi_codegen::FromLuaOwned; use yazi_shim::SStr; @@ -19,9 +19,9 @@ impl Error { let fs = lua.create_function(|_, value: Value| { Ok(Self::Fs(match value { Value::Table(t) => yazi_shim::fs::Error::custom( - &t.raw_get::("kind")?.to_str()?, + &t.raw_get::("kind")?.to_str()?, t.raw_get("code")?, - &t.raw_get::("message")?.to_str()?, + &t.raw_get::("message")?.to_str()?, )?, _ => Err("expected a table".into_lua_err())?, })) diff --git a/yazi-binding/src/permit.rs b/yazi-binding/src/permit.rs index 1224108b..55d5ea5f 100644 --- a/yazi-binding/src/permit.rs +++ b/yazi-binding/src/permit.rs @@ -46,6 +46,6 @@ impl Drop for Permit { impl UserData for Permit { fn add_methods>(methods: &mut M) { - methods.add_async_method_mut("drop", |_, mut me, ()| async move { Ok(me.dropping().await) }); + methods.add_async_method_once("drop", |_, mut me, ()| async move { Ok(me.dropping().await) }); } } diff --git a/yazi-binding/src/position/lua.rs b/yazi-binding/src/position/lua.rs index c76ef309..d2ab5400 100644 --- a/yazi-binding/src/position/lua.rs +++ b/yazi-binding/src/position/lua.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataFields, UserDataMethods, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, LuaString, MetaMethod, Table, UserData, UserDataFields, UserDataMethods, Value}; use yazi_shim::strum::IntoStr; use crate::{elements::Pad, position::{Offset, Origin, Position}}; @@ -20,7 +20,7 @@ impl TryFrom for Position { fn try_from(t: Table) -> Result { Ok(Self { - origin: Origin::from_str(&t.raw_get::(1)?.to_str()?).into_lua_err()?, + origin: Origin::from_str(&t.raw_get::(1)?.to_str()?).into_lua_err()?, offset: Offset { x: t.raw_get("x").unwrap_or_default(), y: t.raw_get("y").unwrap_or_default(), diff --git a/yazi-binding/src/process/child.rs b/yazi-binding/src/process/child.rs index b0550167..e4226408 100644 --- a/yazi-binding/src/process/child.rs +++ b/yazi-binding/src/process/child.rs @@ -1,7 +1,7 @@ use std::{ops::DerefMut, process::ExitStatus, time::Duration}; use futures::future::try_join3; -use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, UserDataMethods, Value}; +use mlua::{ExternalError, IntoLua, IntoLuaMulti, LuaString, Table, UserData, UserDataMethods, Value}; use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use super::Status; @@ -133,7 +133,7 @@ impl UserData for Child { } }); - methods.add_async_method_mut("write_all", |lua, mut me, src: mlua::String| async move { + methods.add_async_method_mut("write_all", |lua, mut me, src: LuaString| async move { let Some(stdin) = &mut me.stdin else { return Err("stdin is not piped".into_lua_err()); }; @@ -158,8 +158,8 @@ impl UserData for Child { Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); - methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move { - match ud.take::()?.wait_with_output().await { + methods.add_async_method_once("wait_with_output", |lua, me, ()| async move { + match me.wait_with_output().await { Ok(output) => Output::new(output).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } diff --git a/yazi-binding/src/process/command.rs b/yazi-binding/src/process/command.rs index 4949b266..21f2dd75 100644 --- a/yazi-binding/src/process/command.rs +++ b/yazi-binding/src/process/command.rs @@ -1,6 +1,6 @@ use std::{any::TypeId, ffi::OsStr, io, process::Stdio}; -use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; +use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Lua, LuaString, MetaMethod, Table, UserData, UserDataMethods, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_shim::wtf8::FromWtf8; @@ -152,7 +152,7 @@ impl UserData for Command { me.inner.arg(OsStr::from_wtf8(&s.as_bytes())?); } Value::Table(t) => { - for s in t.sequence_values::() { + for s in t.sequence_values::() { me.inner.arg(OsStr::from_wtf8(&s?.as_bytes())?); } } @@ -160,19 +160,16 @@ impl UserData for Command { } ud.into_lua(lua) }); - methods.add_function("cwd", |_, (ud, dir): (AnyUserData, mlua::String)| { + methods.add_function("cwd", |_, (ud, dir): (AnyUserData, LuaString)| { ud.borrow_mut::()?.inner.current_dir(dir.to_str()?.as_ref()); Ok(ud) }); - methods.add_function( - "env", - |_, (ud, key, value): (AnyUserData, mlua::String, mlua::String)| { - ud.borrow_mut::()? - .inner - .env(OsStr::from_wtf8(&key.as_bytes())?, OsStr::from_wtf8(&value.as_bytes())?); - Ok(ud) - }, - ); + methods.add_function("env", |_, (ud, key, value): (AnyUserData, LuaString, LuaString)| { + ud.borrow_mut::()? + .inner + .env(OsStr::from_wtf8(&key.as_bytes())?, OsStr::from_wtf8(&value.as_bytes())?); + Ok(ud) + }); methods.add_function("stdin", |_, (ud, stdio): (AnyUserData, Value)| { ud.borrow_mut::()?.inner.stdin(make_stdio(stdio)?); Ok(ud) diff --git a/yazi-codegen/src/lib.rs b/yazi-codegen/src/lib.rs index e50e64ff..d3b247b3 100644 --- a/yazi-codegen/src/lib.rs +++ b/yazi-codegen/src/lib.rs @@ -196,21 +196,13 @@ pub fn overlay(input: TokenStream) -> TokenStream { pub fn from_lua(input: TokenStream) -> TokenStream { let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput); - let ident_str = ident.to_string(); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); quote! { impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { #[inline] - fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result { - match value { - ::mlua::Value::UserData(ud) => ud.take::(), - _ => Err(::mlua::Error::FromLuaConversionError { - from: value.type_name(), - to: #ident_str.to_owned(), - message: None, - }), - } + fn from_lua(value: ::mlua::Value, lua: &::mlua::Lua) -> ::mlua::Result { + <::mlua::UserDataOwned as ::mlua::FromLua>::from_lua(value, lua).map(|ud| ud.0) } } } diff --git a/yazi-config/src/keymap/key.rs b/yazi-config/src/keymap/key.rs index a93f318c..9b9b60d6 100644 --- a/yazi-config/src/keymap/key.rs +++ b/yazi-config/src/keymap/key.rs @@ -19,9 +19,14 @@ pub struct Key { impl Key { pub fn plain(&self) -> Option { - match self.code { - KeyCode::Char(c) if !self.ctrl && !self.alt && !self.super_ => Some(c), - _ => None, + if self.ctrl || self.alt || self.super_ { + None + } else if self.shift && !self.code.implies_shift() { + None + } else if let KeyCode::Char(c) = self.code { + Some(c) + } else { + None } } } @@ -130,7 +135,7 @@ impl Display for Key { if self.alt { write!(f, "A-")?; } - if self.shift && !matches!(self.code, KeyCode::Char(_)) { + if self.shift && !self.code.implies_shift() { write!(f, "S-")?; } diff --git a/yazi-config/src/opener/opener.rs b/yazi-config/src/opener/opener.rs index b175ea5d..5a28c481 100644 --- a/yazi-config/src/opener/opener.rs +++ b/yazi-config/src/opener/opener.rs @@ -2,7 +2,7 @@ use std::{mem, ops::Deref, sync::Arc}; use arc_swap::ArcSwap; use hashbrown::HashMap; -use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, MetaMethod, UserData, UserDataMethods, Value}; +use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, LuaString, MetaMethod, UserData, UserDataMethods, Value}; use serde::{Deserialize, Deserializer}; use yazi_shim::{arc_swap::IntoPointee, toml::{DeserializeOverHook, DeserializeOverWith}}; @@ -86,7 +86,7 @@ impl DeserializeOverWith for Opener { impl UserData for &'static Opener { fn add_methods>(methods: &mut M) { - methods.add_meta_method(MetaMethod::Index, |lua, &me, key: mlua::String| { + methods.add_meta_method(MetaMethod::Index, |lua, &me, key: LuaString| { let key = key.to_str()?; match me.load().get(&*key) { Some(rules) => rules.clone().into_lua(lua), @@ -94,22 +94,19 @@ impl UserData for &'static Opener { } }); - methods.add_meta_method( - MetaMethod::NewIndex, - |lua, &me, (key, value): (mlua::String, Value)| { - let key = key.to_str()?; - match value { - t @ Value::Table(_) => { - me.insert(&key, &OpenerRulesArc::from_lua(t, lua)?); - } - Value::Nil => { - me.remove(&key); - } - _ => return Err("expected a table or nil".into_lua_err()), + methods.add_meta_method(MetaMethod::NewIndex, |lua, &me, (key, value): (LuaString, Value)| { + let key = key.to_str()?; + match value { + t @ Value::Table(_) => { + me.insert(&key, &OpenerRulesArc::from_lua(t, lua)?); } - Ok(()) - }, - ); + Value::Nil => { + me.remove(&key); + } + _ => return Err("expected a table or nil".into_lua_err()), + } + Ok(()) + }); methods.add_meta_method(MetaMethod::Pairs, |lua, &me, ()| { let mut matcher = OpenerRulesMatcher::from(me); diff --git a/yazi-config/src/opener/opener_rule.rs b/yazi-config/src/opener/opener_rule.rs index 71347026..130b10bf 100644 --- a/yazi-config/src/opener/opener_rule.rs +++ b/yazi-config/src/opener/opener_rule.rs @@ -32,15 +32,5 @@ impl OpenerRule { } } - pub fn fill(&mut self) { - #[cfg(unix)] - { - self.spread = - Splatter::<()>::spread(&self.run) || self.run.contains("$@") || self.run.contains("$*"); - } - #[cfg(windows)] - { - self.spread = Splatter::<()>::spread(&self.run) || self.run.contains("%*"); - } - } + pub fn fill(&mut self) { self.spread = Splatter::<()>::spread(&self.run); } } diff --git a/yazi-config/src/theme/custom_section_arc.rs b/yazi-config/src/theme/custom_section_arc.rs index fa26dadc..a63a0c52 100644 --- a/yazi-config/src/theme/custom_section_arc.rs +++ b/yazi-config/src/theme/custom_section_arc.rs @@ -1,7 +1,7 @@ use std::{ops::Deref, sync::Arc}; use hashbrown::HashMap; -use mlua::{MetaMethod, UserData, UserDataMethods}; +use mlua::{LuaString, MetaMethod, UserData, UserDataMethods}; use yazi_shared::SnakeCasedString; use crate::theme::{CustomField, CustomSection}; @@ -20,7 +20,7 @@ impl From<&CustomSection> for CustomSectionArc { impl UserData for CustomSectionArc { fn add_methods>(methods: &mut M) { - methods.add_meta_method(MetaMethod::Index, |_, me, key: mlua::String| { + methods.add_meta_method(MetaMethod::Index, |_, me, key: LuaString| { Ok(me.get(&*key.to_str()?).cloned()) }); } diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index fbe8fbfb..f7eea0fc 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -25,7 +25,7 @@ pub struct Help { } impl Help { - pub fn r#type(&mut self, key: KeyEvent) -> Result { + pub fn r#type(&mut self, key: &KeyEvent) -> Result { if !self.input.r#type(key)? { return Ok(false); } diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index 99f60a3c..28c1cf32 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -1,9 +1,6 @@ -use std::iter; - use ratatui_core::layout::Rect; use yazi_binding::position::{Origin, Position}; -use yazi_fs::Splatable; -use yazi_shared::url::{AsUrl, Url, UrlBuf}; +use yazi_shared::url::UrlBuf; use yazi_term::TERM; use yazi_watcher::Watcher; @@ -52,35 +49,12 @@ impl Mgr { #[inline] pub fn active_mut(&mut self) -> &mut Tab { self.tabs.active_mut() } + #[inline] + pub fn current(&self) -> &Folder { &self.active().current } + #[inline] pub fn current_mut(&mut self) -> &mut Folder { &mut self.active_mut().current } #[inline] pub fn parent_mut(&mut self) -> Option<&mut Folder> { self.active_mut().parent.as_mut() } } - -impl Splatable for Mgr { - fn tab(&self) -> usize { self.tabs.cursor } - - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { - idx = idx.and_then(|i| i.checked_sub(1)); - tab - .checked_sub(1) - .and_then(|tab| self.tabs.get(tab)) - .map(|tab| tab.selected_or_hovered_urls()) - .unwrap_or_else(|| Box::new(iter::empty())) - .skip(idx.unwrap_or(0)) - .take(if idx.is_some() { 1 } else { usize::MAX }) - .map(|u| u.as_url()) - } - - fn hovered(&self, tab: usize) -> Option> { - tab - .checked_sub(1) - .and_then(|tab| self.tabs.get(tab)) - .and_then(|tab| tab.hovered()) - .map(|h| h.url.as_url()) - } - - fn yanked(&self) -> impl Iterator> { self.yanked.iter().map(|f| f.url.as_url()) } -} diff --git a/yazi-core/src/mgr/mod.rs b/yazi-core/src/mgr/mod.rs index b1816933..9076722d 100644 --- a/yazi-core/src/mgr/mod.rs +++ b/yazi-core/src/mgr/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(batcher cd displace filter find mgr mimetype open search tabs yanked); +yazi_macro::mod_flat!(batcher cd displace filter find mgr mimetype open search snap tabs yanked); diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs new file mode 100644 index 00000000..f726bd56 --- /dev/null +++ b/yazi-core/src/mgr/snap.rs @@ -0,0 +1,54 @@ +use yazi_fs::Splatable; +use yazi_shared::url::{AsUrl, Url, UrlBuf}; + +use crate::{mgr::Mgr, tab::TabSnap}; + +pub struct MgrSnap { + tab: usize, + tabs: Vec, + yanked: Vec, +} + +impl From<&Mgr> for MgrSnap { + fn from(value: &Mgr) -> Self { + Self { + tab: value.tabs.cursor, + tabs: value.tabs.iter().map(Into::into).collect(), + yanked: value.yanked.urls().cloned().collect(), + } + } +} + +impl Splatable for MgrSnap { + fn tab(&self) -> usize { self.tab + 1 } + + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { + idx = idx.and_then(|i| i.checked_sub(1)); + tab + .checked_sub(1) + .and_then(|tab| self.tabs.get(tab)) + .map_or_else(|| &[][..], |s| &s.selected) + .into_iter() + .skip(idx.unwrap_or(0)) + .take(if idx.is_some() { 1 } else { usize::MAX }) + .map(AsUrl::as_url) + } + + fn hovered(&self, tab: usize) -> Option> { + tab + .checked_sub(1) + .and_then(|tab| self.tabs.get(tab)) + .and_then(|tab| tab.hovered.as_ref()) + .map(AsUrl::as_url) + } + + fn yanked(&self, mut idx: Option) -> impl Iterator> { + idx = idx.and_then(|i| i.checked_sub(1)); + self + .yanked + .iter() + .skip(idx.unwrap_or(0)) + .take(if idx.is_some() { 1 } else { usize::MAX }) + .map(AsUrl::as_url) + } +} diff --git a/yazi-core/src/spot/lock.rs b/yazi-core/src/spot/lock.rs index 30f11ae5..1793f095 100644 --- a/yazi-core/src/spot/lock.rs +++ b/yazi-core/src/spot/lock.rs @@ -1,13 +1,13 @@ use mlua::Table; -use yazi_fs::file::FileRef; +use yazi_fs::{cha::Cha, file::FileRef}; use yazi_macro::impl_data_any; -use yazi_shared::id::Id; +use yazi_shared::{id::Id, url::UrlBuf}; use yazi_widgets::Renderable; #[derive(Clone, Debug)] pub struct SpotLock { - pub url: yazi_shared::url::UrlBuf, - pub cha: yazi_fs::cha::Cha, + pub url: UrlBuf, + pub cha: Cha, pub mime: String, pub id: Id, diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 49d8d63a..60f68565 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -169,6 +169,9 @@ impl Folder { #[inline] pub fn hovered_mut(&mut self) -> Option<&mut File> { self.entries.get_mut(self.cursor) } + #[inline] + pub fn hovered_url(&self) -> Option<&UrlBuf> { self.hovered().map(|f| &f.url) } + pub fn paginate(&self, page: usize) -> &[File] { let len = self.entries.len(); let limit = LAYOUT.get().folder_limit(); diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index 8f7666e1..508b8072 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(backstack finder folder history visual mode preference preview preview_lock selected tab); +yazi_macro::mod_flat!(backstack finder folder history mode preference preview preview_lock selected snap tab visual); diff --git a/yazi-core/src/tab/preview_lock.rs b/yazi-core/src/tab/preview_lock.rs index 243bec31..a7a264f6 100644 --- a/yazi-core/src/tab/preview_lock.rs +++ b/yazi-core/src/tab/preview_lock.rs @@ -1,14 +1,14 @@ -use mlua::Table; +use mlua::{LuaString, Table}; use yazi_binding::elements::Rect; -use yazi_fs::file::FileRef; +use yazi_fs::{cha::Cha, file::FileRef}; use yazi_macro::impl_data_any; -use yazi_shared::pool::{InternStr, Symbol}; +use yazi_shared::{pool::{InternStr, Symbol}, url::UrlBuf}; use yazi_widgets::Renderable; #[derive(Clone, Debug, Default)] pub struct PreviewLock { - pub url: yazi_shared::url::UrlBuf, - pub cha: yazi_fs::cha::Cha, + pub url: UrlBuf, + pub cha: Cha, pub mime: Symbol, pub skip: usize, @@ -27,7 +27,7 @@ impl TryFrom
for PreviewLock { Ok(Self { url: f.url_owned(), cha: f.cha, - mime: t.raw_get::("mime")?.to_str()?.intern(), + mime: t.raw_get::("mime")?.to_str()?.intern(), skip: t.raw_get("skip")?, area: t.raw_get("area")?, diff --git a/yazi-core/src/tab/snap.rs b/yazi-core/src/tab/snap.rs new file mode 100644 index 00000000..5d59bfd2 --- /dev/null +++ b/yazi-core/src/tab/snap.rs @@ -0,0 +1,17 @@ +use yazi_shared::url::UrlBuf; + +use crate::tab::Tab; + +pub struct TabSnap { + pub hovered: Option, + pub selected: Vec, +} + +impl From<&Tab> for TabSnap { + fn from(value: &Tab) -> Self { + Self { + hovered: value.hovered_url().cloned(), + selected: value.selected.urls().cloned().collect(), + } + } +} diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 6fd8660d..958ba927 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -78,6 +78,9 @@ impl Tab { #[inline] pub fn hovered(&self) -> Option<&File> { self.current.hovered() } + #[inline] + pub fn hovered_url(&self) -> Option<&UrlBuf> { self.current.hovered_url() } + #[inline] pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } @@ -109,7 +112,7 @@ impl Tab { pub fn selected_or_hovered_urls(&self) -> Box + '_> { if self.selected.is_empty() { - Box::new(self.hovered().map(|h| &h.url).into_iter()) + Box::new(self.hovered_url().into_iter()) } else { Box::new(self.selected.urls()) } diff --git a/yazi-core/src/tasks/mod.rs b/yazi-core/src/tasks/mod.rs index 888cc397..f43e59d6 100644 --- a/yazi-core/src/tasks/mod.rs +++ b/yazi-core/src/tasks/mod.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_flat!(file option prework process tasks); +yazi_macro::mod_flat!(file option prework tasks); pub const TASKS_BORDER: u16 = 2; pub const TASKS_PADDING: u16 = 2; diff --git a/yazi-core/src/tasks/process.rs b/yazi-core/src/tasks/process.rs deleted file mode 100644 index c403d2a7..00000000 --- a/yazi-core/src/tasks/process.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::mem; - -use yazi_fs::Splatter; -use yazi_scheduler::process::ProcessOpt; - -use super::Tasks; - -impl Tasks { - // TODO: remove - pub fn open_shell_compat(&self, mut opt: ProcessOpt) { - if opt.spread { - opt.cmd = Splatter::new(&opt.args).splat(opt.cmd); - self.scheduler.process_open(opt); - return; - } - if opt.args.is_empty() { - return; - } - if opt.args.len() == 2 { - opt.cmd = Splatter::new(&opt.args).splat(opt.cmd); - self.scheduler.process_open(opt); - return; - } - let hovered = mem::take(&mut opt.args[0]); - for target in opt.args.into_iter().skip(1) { - let args = vec![hovered.clone(), target]; - self.scheduler.process_open(ProcessOpt { - cwd: opt.cwd.clone(), - cmd: Splatter::new(&args).splat(&opt.cmd), - args, - block: opt.block, - orphan: opt.orphan, - spread: opt.spread, - }); - } - } -} diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index f8f04d3d..ea383ee5 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -59,7 +59,7 @@ libc = { workspace = true } signal-hook-tokio = { version = "0.4.0", features = [ "futures-v0_3" ] } [target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies] -tikv-jemallocator = "0.6.1" +tikv-jemallocator = "0.7.0" [[bin]] name = "yazi" diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 61e06097..f2efbbca 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -182,7 +182,6 @@ impl<'a> Executor<'a> { on!(inspect); on!(cancel); on!(process_open); - on!(open_shell_compat); match action.name.as_ref() { // Help diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index d28812a6..96ca463b 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -19,12 +19,12 @@ impl<'a> Router<'a> { use Layer as L; let core = &mut self.app.core; - if core.help.visible && core.help.r#type(key)? { + if core.help.visible && core.help.r#type(&key)? { return Ok(true); } if let Some(mut guard) = core.input.lock_mut() - && guard.r#type(key)? + && guard.r#type(&key)? { return Ok(true); } diff --git a/yazi-fs/src/splatter.rs b/yazi-fs/src/splatter.rs index 9db2b6cf..42cd4af5 100644 --- a/yazi-fs/src/splatter.rs +++ b/yazi-fs/src/splatter.rs @@ -4,7 +4,7 @@ use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem}; -use yazi_shared::url::{AsUrl, Url, UrlCow}; +use yazi_shared::url::{AsUrl, Url}; use crate::FsUrl; @@ -31,7 +31,7 @@ pub trait Splatable { fn hovered(&self, tab: usize) -> Option>; - fn yanked(&self) -> impl Iterator>; + fn yanked(&self, idx: Option) -> impl Iterator>; } #[cfg(unix)] @@ -50,7 +50,7 @@ impl Splatter where T: Splatable, { - pub fn new(src: T) -> Self { Self { tab: src.tab() + 1, src } } + pub fn new(src: T) -> Self { Self { tab: src.tab(), src } } pub fn splat(mut self, cmd: impl AsRef) -> OsString { #[cfg(unix)] @@ -82,8 +82,6 @@ where Some('t') | Some('T') => self.visit_tab(it, buf), Some('y') | Some('Y') => self.visit_yanked(it, buf), Some('%') => self.visit_escape(it, buf), - Some('*') => self.visit_selected(it, buf), // TODO: remove this - Some(c) if c.is_ascii_digit() => self.visit_digit(it, buf), _ => self.visit_unknown(it, buf), } } @@ -154,32 +152,12 @@ where self.tab = old; } - fn visit_digit(&mut self, it: &mut Iter, buf: &mut Buf) { - // TODO: remove - match self.consume_digit(it) { - Some(0) => { - cue(buf, self.src.hovered(self.tab).map(|u| u.unified_path_str()).unwrap_or_default()); - } - Some(n) => { - cue( - buf, - self - .src - .selected(self.tab, Some(n)) - .next() - .map(|u| u.unified_path_str()) - .unwrap_or_default(), - ); - } - None => unreachable!(), - } - } - fn visit_yanked(&mut self, it: &mut Iter, buf: &mut Buf) { let c = it.next().and_then(b2c); + let idx = self.consume_digit(it); let mut first = true; - for url in self.src.yanked() { + for url in self.src.yanked(idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } @@ -190,6 +168,9 @@ where cue(buf, url.unified_path_str()); } } + if first && idx.is_some() { + cue(buf, ""); + } } fn visit_escape(&mut self, it: &mut Iter, buf: &mut Buf) { buf.push(it.next().unwrap()); } @@ -232,10 +213,7 @@ impl Splatter { fn hovered(&self, _tab: usize) -> Option> { None } - fn yanked(&self) -> impl Iterator> { - self.0.set(true); - iter::empty() - } + fn yanked(&self, _idx: Option) -> impl Iterator> { iter::empty() } } let src = Source(Cell::new(false)); @@ -244,28 +222,27 @@ impl Splatter { } } -// TODO: remove -impl<'a, T> Splatable for &'a T +impl<'a, I, T> Splatable for &'a I where - T: AsRef<[UrlCow<'a>]>, + I: ?Sized, + &'a I: IntoIterator, + T: AsUrl + 'a, { - fn tab(&self) -> usize { 0 } + fn tab(&self) -> usize { 1 } - fn selected(&self, tab: usize, idx: Option) -> impl Iterator> { - self - .as_ref() - .iter() + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { + idx = idx.and_then(|i| i.checked_sub(1)); + (*self) + .into_iter() .filter(move |_| tab == 1) .map(|u| u.as_url()) - .skip(idx.unwrap_or(1)) + .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) } - fn hovered(&self, tab: usize) -> Option> { - self.as_ref().first().filter(|_| tab == 1).map(|u| u.as_url()) - } + fn hovered(&self, _tab: usize) -> Option> { None } - fn yanked(&self) -> impl Iterator> { iter::empty() } + fn yanked(&self, _idx: Option) -> impl Iterator> { iter::empty() } } #[cfg(test)] @@ -300,8 +277,12 @@ mod tests { } } - fn yanked(&self) -> impl Iterator> { - [Url::regular("y1"), Url::regular("y 2"), Url::regular("y3")].into_iter() + fn yanked(&self, mut idx: Option) -> impl Iterator> { + idx = idx.and_then(|i| i.checked_sub(1)); + [Url::regular("y1"), Url::regular("y 2"), Url::regular("y3")] + .into_iter() + .skip(idx.unwrap_or(0)) + .take(if idx.is_some() { 1 } else { usize::MAX }) } } @@ -310,41 +291,40 @@ mod tests { fn test_unix() { let cases = [ // Selected - (Source(0), r#"ls %s"#, r#"ls t1/s1 t1/s2"#), - (Source(0), r#"ls %s1 %s2 %s3"#, r#"ls t1/s1 t1/s2 ''"#), - (Source(0), r#"ls %s %s2 %s"#, r#"ls t1/s1 t1/s2 t1/s2 t1/s1 t1/s2"#), - (Source(1), r#"ls %s"#, r#"ls 't 2/s 1' 't 2/s 2'"#), - (Source(1), r#"ls %s1 %s3 %s2"#, r#"ls 't 2/s 1' '' 't 2/s 2'"#), - (Source(2), r#"ls %s"#, r#"ls "#), - (Source(2), r#"ls %s1 %s %s2"#, r#"ls '' ''"#), + (Source(1), r#"ls %s"#, r#"ls t1/s1 t1/s2"#), + (Source(1), r#"ls %s1 %s2 %s3"#, r#"ls t1/s1 t1/s2 ''"#), + (Source(1), r#"ls %s %s2 %s"#, r#"ls t1/s1 t1/s2 t1/s2 t1/s1 t1/s2"#), + (Source(2), r#"ls %s"#, r#"ls 't 2/s 1' 't 2/s 2'"#), + (Source(2), r#"ls %s1 %s3 %s2"#, r#"ls 't 2/s 1' '' 't 2/s 2'"#), + (Source(3), r#"ls %s"#, r#"ls "#), + (Source(3), r#"ls %s1 %s %s2"#, r#"ls '' ''"#), // Hovered - (Source(0), r#"ls %h"#, r#"ls hovered"#), - (Source(1), r#"ls %h"#, r#"ls 'hover ed'"#), - (Source(2), r#"ls %h"#, r#"ls ''"#), + (Source(1), r#"ls %h"#, r#"ls hovered"#), + (Source(2), r#"ls %h"#, r#"ls 'hover ed'"#), + (Source(3), r#"ls %h"#, r#"ls ''"#), // Dirname - (Source(0), r#"cd %d"#, r#"cd t1 t1"#), - (Source(1), r#"cd %d"#, r#"cd 't 2' 't 2'"#), - (Source(1), r#"cd %d1 %d3 %d2"#, r#"cd 't 2' '' 't 2'"#), - (Source(2), r#"cd %d %d1"#, r#"cd ''"#), + (Source(1), r#"cd %d"#, r#"cd t1 t1"#), + (Source(2), r#"cd %d"#, r#"cd 't 2' 't 2'"#), + (Source(2), r#"cd %d1 %d3 %d2"#, r#"cd 't 2' '' 't 2'"#), + (Source(3), r#"cd %d %d1"#, r#"cd ''"#), // Yanked - (Source(0), r#"cd %y"#, r#"cd y1 'y 2' y3"#), (Source(1), r#"cd %y"#, r#"cd y1 'y 2' y3"#), (Source(2), r#"cd %y"#, r#"cd y1 'y 2' y3"#), + (Source(3), r#"cd %y"#, r#"cd y1 'y 2' y3"#), + (Source(1), r#"cd %y1 %y3 %y2 %y4"#, r#"cd y1 y3 'y 2' ''"#), // Tab - (Source(0), r#"ls %s %ts %s"#, r#"ls t1/s1 t1/s2 't 2/s 1' 't 2/s 2' t1/s1 t1/s2"#), - (Source(1), r#"ls %s1 %ts %s2"#, r#"ls 't 2/s 1' 't 2/s 2'"#), - (Source(1), r#"ls %s1 %Ts1 %s2 %Ts2"#, r#"ls 't 2/s 1' t1/s1 't 2/s 2' t1/s2"#), - (Source(0), r#"ls %s1 %Ts1 %s2 %Ts2"#, r#"ls t1/s1 '' t1/s2 ''"#), - (Source(0), r#"ls %ty"#, r#"ls y1 'y 2' y3"#), - (Source(0), r#"ls %Ty"#, r#"ls y1 'y 2' y3"#), + (Source(1), r#"ls %s %ts %s"#, r#"ls t1/s1 t1/s2 't 2/s 1' 't 2/s 2' t1/s1 t1/s2"#), + (Source(2), r#"ls %s1 %ts %s2"#, r#"ls 't 2/s 1' 't 2/s 2'"#), + (Source(2), r#"ls %s1 %Ts1 %s2 %Ts2"#, r#"ls 't 2/s 1' t1/s1 't 2/s 2' t1/s2"#), + (Source(1), r#"ls %s1 %Ts1 %s2 %Ts2"#, r#"ls t1/s1 '' t1/s2 ''"#), + (Source(1), r#"ls %ty"#, r#"ls y1 'y 2' y3"#), + (Source(1), r#"ls %Ty"#, r#"ls y1 'y 2' y3"#), // Escape ( - Source(0), + Source(1), r#"echo % %% %s2 %%h %d %%%y %%%%ts %%%%%ts1"#, r#"echo % % t1/s2 %h t1 t1 %y1 'y 2' y3 %%ts %%'t 2/s 1'"#, ), - // TODO: remove - (Source(0), r#"ls %1 %* %2 %0 %3"#, r#"ls t1/s1 t1/s1 t1/s2 t1/s2 hovered ''"#), ]; for (src, cmd, expected) in cases { diff --git a/yazi-parser/src/tasks/process_open.rs b/yazi-parser/src/tasks/process_open.rs index 5d2c6e2a..a1717884 100644 --- a/yazi-parser/src/tasks/process_open.rs +++ b/yazi-parser/src/tasks/process_open.rs @@ -1,11 +1,11 @@ use anyhow::anyhow; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_scheduler::process::ProcessOpt; +use yazi_scheduler::process::ShellOpt; use yazi_shared::event::{ActionCow, Replier}; #[derive(Clone, Debug)] pub struct ProcessOpenForm { - pub opt: ProcessOpt, + pub opt: ShellOpt, pub replier: Option, } diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 7d7d8311..404b6b2b 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; +use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, LuaString, Table, Value}; use yazi_binding::{Composer, ComposerGet, ComposerSet, Error}; use yazi_config::Pattern; use yazi_fs::{file::File, mounts::PARTITIONS, provider::{Attrs, DirReader, FileHolder}}; @@ -81,7 +81,7 @@ fn copy(lua: &Lua) -> mlua::Result { } fn create(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { + lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move { let result = match &*r#type.as_bytes() { b"dir" => provider::create_dir(&*url).await, b"dir_all" => provider::create_dir_all(&*url).await, @@ -132,7 +132,7 @@ fn file(lua: &Lua) -> mlua::Result { } fn op(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (name, t): (mlua::String, Table)| match &*name.as_bytes() { + lua.create_function(|lua, (name, t): (LuaString, Table)| match &*name.as_bytes() { b"part" => super::FilesOp::part(lua, t), b"done" => super::FilesOp::done(lua, t), b"size" => super::FilesOp::size(lua, t), @@ -162,7 +162,7 @@ fn partitions(lua: &Lua) -> mlua::Result { fn read_dir(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move { - let pat = if let Ok(s) = options.raw_get::("glob") { + let pat = if let Ok(s) = options.raw_get::("glob") { Some(Pattern::from_str(&s.to_str()?)?) } else { None @@ -202,7 +202,7 @@ fn read_dir(lua: &Lua) -> mlua::Result { } fn remove(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { + lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move { let result = match &*r#type.as_bytes() { b"file" => provider::remove_file(&*url).await, b"dir" => provider::remove_dir(&*url).await, @@ -228,7 +228,7 @@ fn rename(lua: &Lua) -> mlua::Result { } fn unique(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { + lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move { let result = match &*r#type.as_bytes() { b"dir" => yazi_vfs::unique_file(url.clone(), true).await, b"file" => yazi_vfs::unique_file(url.clone(), false).await, @@ -243,7 +243,7 @@ fn unique(lua: &Lua) -> mlua::Result { } fn write(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move { + lua.create_async_function(|lua, (url, data): (UrlRef, LuaString)| async move { match provider::write(&*url, data.as_bytes()).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), diff --git a/yazi-plugin/src/fs/op.rs b/yazi-plugin/src/fs/op.rs index d894985a..b613eb9e 100644 --- a/yazi-plugin/src/fs/op.rs +++ b/yazi-plugin/src/fs/op.rs @@ -3,7 +3,7 @@ use yazi_codegen::FromLuaOwned; use yazi_macro::impl_data_any; use yazi_shared::url::UrlBuf; -#[derive(Clone, FromLuaOwned)] +#[derive(Clone, FromLuaOwned, UserData)] pub(super) struct FilesOp(yazi_fs::FilesOp); impl_data_any!(FilesOp => yazi_fs::FilesOp; from_into_lua = inherit); @@ -44,5 +44,3 @@ impl FilesOp { Ok(Self(yazi_fs::FilesOp::Size(url, sizes.pairs().collect::>()?))) } } - -impl UserData for FilesOp {} diff --git a/yazi-plugin/src/pubsub/pubsub.rs b/yazi-plugin/src/pubsub/pubsub.rs index 21c1388a..2ab83063 100644 --- a/yazi-plugin/src/pubsub/pubsub.rs +++ b/yazi-plugin/src/pubsub/pubsub.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalResult, Function, Lua, Value}; +use mlua::{ExternalResult, Function, Lua, LuaString, Value}; use yazi_binding::runtime; use yazi_dds::ember::Ember; use yazi_shared::id::Id; @@ -7,20 +7,20 @@ pub struct Pubsub; impl Pubsub { pub(super) fn r#pub(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (kind, value): (mlua::String, Value)| { + lua.create_function(|lua, (kind, value): (LuaString, Value)| { yazi_dds::Pubsub::r#pub(Ember::from_lua(lua, &kind.to_str()?, value)?).into_lua_err() }) } pub(super) fn pub_to(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (receiver, kind, value): (Id, mlua::String, Value)| { + lua.create_function(|lua, (receiver, kind, value): (Id, LuaString, Value)| { yazi_dds::Pubsub::pub_to(receiver, Ember::from_lua(lua, &kind.to_str()?, value)?) .into_lua_err() }) } pub(super) fn sub(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (kind, f): (mlua::String, Function)| { + lua.create_function(|lua, (kind, f): (LuaString, Function)| { let rt = runtime!(lua)?; if !yazi_dds::Pubsub::sub(rt.current()?, &kind.to_str()?, f) { return Err("`sub()` called twice").into_lua_err(); @@ -30,7 +30,7 @@ impl Pubsub { } pub(super) fn sub_remote(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (kind, f): (mlua::String, Function)| { + lua.create_function(|lua, (kind, f): (LuaString, Function)| { let rt = runtime!(lua)?; if !yazi_dds::Pubsub::sub_remote(rt.current()?, &kind.to_str()?, f) { return Err("`sub_remote()` called twice").into_lua_err(); @@ -40,14 +40,14 @@ impl Pubsub { } pub(super) fn unsub(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, kind: mlua::String| { + lua.create_function(|lua, kind: LuaString| { let rt = runtime!(lua)?; Ok(yazi_dds::Pubsub::unsub(rt.current()?, &kind.to_str()?)) }) } pub(super) fn unsub_remote(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, kind: mlua::String| { + lua.create_function(|lua, kind: LuaString| { let rt = runtime!(lua)?; Ok(yazi_dds::Pubsub::unsub_remote(rt.current()?, &kind.to_str()?)) }) diff --git a/yazi-plugin/src/tasks/option.rs b/yazi-plugin/src/tasks/option.rs index fd15e878..4252d18d 100644 --- a/yazi-plugin/src/tasks/option.rs +++ b/yazi-plugin/src/tasks/option.rs @@ -1,4 +1,4 @@ -use mlua::{AnyUserData, UserData, UserDataMethods}; +use mlua::{AnyUserData, LuaString, UserData, UserDataMethods}; use yazi_proxy::TasksProxy; use yazi_scheduler::TaskIn; @@ -9,7 +9,7 @@ pub(crate) struct TaskOpt(pub(crate) yazi_core::tasks::TaskOpt); impl UserData for TaskOpt { fn add_methods>(methods: &mut M) { - methods.add_function("name", |_, (ud, name): (AnyUserData, mlua::String)| { + methods.add_function("name", |_, (ud, name): (AnyUserData, LuaString)| { ud.borrow_mut::()?.0.set_title(name.to_string_lossy()); Ok(ud) }); diff --git a/yazi-plugin/src/ui/utils.rs b/yazi-plugin/src/ui/utils.rs index b5a61827..6252cf01 100644 --- a/yazi-plugin/src/ui/utils.rs +++ b/yazi-plugin/src/ui/utils.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, iter}; use ansi_to_tui::IntoText; -use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, ObjectLike, Table, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, LuaString, ObjectLike, Table, Value}; use tracing::error; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use yazi_binding::{Permit, PermitRef, elements::{Line, Rect, Span, Wrap}, runtime}; @@ -12,7 +12,7 @@ use yazi_shim::ratatui::LineIter; use yazi_term::YIELD_TO_SUBPROCESS; pub(super) fn area(lua: &Lua) -> mlua::Result { - let f = lua.create_function(|_, s: mlua::String| { + let f = lua.create_function(|_, s: LuaString| { let layout = LAYOUT.get(); Ok(match &*s.as_bytes() { b"current" => Rect(layout.current), @@ -46,7 +46,7 @@ pub(super) fn hide(lua: &Lua) -> mlua::Result { } pub(super) fn lines(lua: &Lua) -> mlua::Result { - let f = lua.create_function(|lua, (s, opts): (mlua::String, Table)| { + let f = lua.create_function(|lua, (s, opts): (LuaString, Table)| { let b = s.as_bytes(); let s = &*String::from_utf8_lossy(&b); @@ -74,7 +74,7 @@ pub(super) fn lines(lua: &Lua) -> mlua::Result { } pub(super) fn printable(lua: &Lua) -> mlua::Result { - let f = lua.create_function(|lua, s: mlua::String| { + let f = lua.create_function(|lua, s: LuaString| { Ok(match replace_to_printable(&s.as_bytes(), false, 1, true) { Cow::Borrowed(_) => s, Cow::Owned(new) => lua.create_external_string(new)?, @@ -86,7 +86,7 @@ pub(super) fn printable(lua: &Lua) -> mlua::Result { pub(super) fn redraw(lua: &Lua) -> mlua::Result { let f = lua.create_function(|lua, c: Table| { - let id: mlua::String = c.get("_id")?; + let id: LuaString = c.get("_id")?; let mut layout = LAYOUT.get(); match &*id.as_bytes() { @@ -135,7 +135,7 @@ pub(super) fn truncate(lua: &Lua) -> mlua::Result { (idx, last, adv > max) } - let f = lua.create_function(|lua, (s, t): (mlua::String, Table)| { + let f = lua.create_function(|lua, (s, t): (LuaString, Table)| { let b = s.as_bytes(); if b.is_empty() { return Ok(s); diff --git a/yazi-plugin/src/utils/app.rs b/yazi-plugin/src/utils/app.rs index ca760651..d3d0a431 100644 --- a/yazi-plugin/src/utils/app.rs +++ b/yazi-plugin/src/utils/app.rs @@ -1,6 +1,6 @@ use std::any::TypeId; -use mlua::{AnyUserData, ExternalError, Function, Lua}; +use mlua::{AnyUserData, ExternalError, Function, Lua, LuaString}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_vfs::provider::RwFile; @@ -8,7 +8,7 @@ use super::Utils; impl Utils { pub(super) fn id(lua: &Lua) -> mlua::Result { - lua.create_function(|_, r#type: mlua::String| { + lua.create_function(|_, r#type: LuaString| { Ok(match &*r#type.as_bytes() { b"app" => *yazi_dds::ID, b"ft" => yazi_fs::FILES_TICKET.next(), diff --git a/yazi-plugin/src/utils/json.rs b/yazi-plugin/src/utils/json.rs index 447e5e82..b2211bec 100644 --- a/yazi-plugin/src/utils/json.rs +++ b/yazi-plugin/src/utils/json.rs @@ -1,4 +1,4 @@ -use mlua::{Function, IntoLuaMulti, Lua, LuaSerdeExt, Value}; +use mlua::{Function, IntoLuaMulti, Lua, LuaSerdeExt, LuaString, Value}; use yazi_binding::Error; use yazi_shim::mlua::SER_OPT; @@ -15,7 +15,7 @@ impl Utils { } pub(super) fn json_decode(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, s: mlua::String| async move { + lua.create_async_function(|lua, s: LuaString| async move { match serde_json::from_slice::(&s.as_bytes()) { Ok(v) => lua.to_value_with(&v, SER_OPT)?.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Serde(e)).into_lua_multi(&lua), diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index 1057189b..acb65c5b 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -1,6 +1,6 @@ use std::{str::FromStr, time::Duration}; -use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, Table, Value}; +use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, LuaString, Table, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; use yazi_binding::{elements::{Line, Text}, runtime}; use yazi_config::{Platform, keymap::{Chord, ChordArc, Key}, popup::ConfirmCfg}; @@ -99,7 +99,7 @@ impl Utils { } Value::Table(t) => { let mut v = Vec::with_capacity(10); - for s in t.sequence_values::() { + for s in t.sequence_values::() { v.push(Key::from_str(&s?.to_str()?).into_lua_err()?); } v diff --git a/yazi-plugin/src/utils/log.rs b/yazi-plugin/src/utils/log.rs index e1dc93c2..b34d6275 100644 --- a/yazi-plugin/src/utils/log.rs +++ b/yazi-plugin/src/utils/log.rs @@ -1,8 +1,7 @@ -use std::{any::TypeId, fmt::Write}; +use std::fmt::Write; -use mlua::{Function, Lua, MultiValue, Value}; +use mlua::{Function, Lua, MultiValue}; use tracing::{debug, error}; -use yazi_shared::{id::Id, path::PathBufDyn, url::UrlBuf}; use super::Utils; @@ -21,27 +20,8 @@ impl Utils { if !s.is_empty() { s.push(' '); } - Self::format_one(&mut s, value)?; + write!(s, "{value:#?}")?; } Ok(s) } - - fn format_one(buf: &mut String, value: Value) -> anyhow::Result<()> { - let Value::UserData(ud) = &value else { - return Ok(write!(buf, "{value:#?}")?); - }; - - let id = ud.type_id(); - let ptr = ud.to_pointer(); - Ok(match id { - Some(t) if t == TypeId::of::() => { - write!(buf, "Url({ptr:?}): {:?}", *ud.borrow::()?)? - } - Some(t) if t == TypeId::of::() => { - write!(buf, "Path({ptr:?}): {:?}", *ud.borrow::()?)? - } - Some(t) if t == TypeId::of::() => write!(buf, "Id({ptr:?}): {}", *ud.borrow::()?)?, - _ => write!(buf, "{value:#?}")?, - }) - } } diff --git a/yazi-plugin/src/utils/sync.rs b/yazi-plugin/src/utils/sync.rs index e8ebf7da..0de4dfa1 100644 --- a/yazi-plugin/src/utils/sync.rs +++ b/yazi-plugin/src/utils/sync.rs @@ -1,6 +1,6 @@ use anyhow::Context; use futures::future::join_all; -use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, MultiValue, Table, Value, Variadic}; +use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, LuaString, MultiValue, Table, Value, Variadic}; use tokio::sync::mpsc; use yazi_binding::{Handle, MpscRx, MpscTx, MpscUnboundedRx, MpscUnboundedTx, OneshotRx, OneshotTx, runtime, runtime_mut}; use yazi_core::{AppProxy, app::PluginOpt}; @@ -88,7 +88,7 @@ impl Utils { } pub(super) fn chan(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (r#type, buffer): (mlua::String, Option)| { + lua.create_function(|lua, (r#type, buffer): (LuaString, Option)| { match (&*r#type.as_bytes(), buffer) { (b"mpsc", Some(buffer)) if buffer < 1 => { Err("Buffer size must be greater than 0".into_lua_err()) @@ -103,7 +103,7 @@ impl Utils { } (b"oneshot", _) => { let (tx, rx) = tokio::sync::oneshot::channel::(); - (OneshotTx(Some(tx)), OneshotRx(Some(rx))).into_lua_multi(lua) + (OneshotTx(tx), OneshotRx(rx)).into_lua_multi(lua) } _ => Err("Channel type must be `mpsc` or `oneshot`".into_lua_err()), } diff --git a/yazi-plugin/src/utils/tasks.rs b/yazi-plugin/src/utils/tasks.rs index 4c5c63e7..fcf0d108 100644 --- a/yazi-plugin/src/utils/tasks.rs +++ b/yazi-plugin/src/utils/tasks.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalError, FromLua, Function, Lua, Value}; +use mlua::{ExternalError, FromLua, Function, Lua, LuaString, Value}; use yazi_core::tasks; use super::Utils; @@ -6,7 +6,7 @@ use crate::tasks::TaskOpt; impl Utils { pub(super) fn task(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (kind, value): (mlua::String, Value)| { + lua.create_function(|lua, (kind, value): (LuaString, Value)| { Ok(TaskOpt(match &*kind.as_bytes() { b"cut" => tasks::TaskOpt::Cut(<_>::from_lua(value, lua)?), diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index f5522195..f9523424 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use mlua::{Function, Lua}; +use mlua::{Function, Lua, LuaString}; use twox_hash::XxHash3_128; use yazi_shim::RFC_3986; use yazi_widgets::CLIPBOARD; @@ -9,13 +9,13 @@ use super::Utils; impl Utils { pub(super) fn hash(lua: &Lua) -> mlua::Result { - lua.create_async_function(move |_, s: mlua::String| async move { + lua.create_async_function(move |_, s: LuaString| async move { Ok(format!("{:x}", XxHash3_128::oneshot(&s.as_bytes()))) }) } pub(super) fn quote(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (s, unix): (mlua::String, Option)| { + lua.create_function(|lua, (s, unix): (LuaString, Option)| { let b = s.as_bytes(); let s = match unix { Some(true) => yazi_shared::shell::unix::escape_os_bytes(&b), @@ -38,7 +38,7 @@ impl Utils { } pub(super) fn percent_decode(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, s: mlua::String| { + lua.create_function(|lua, s: LuaString| { let b = s.as_bytes(); match percent_encoding::percent_decode(&b).into() { Cow::Borrowed(_) => Ok(s), @@ -48,7 +48,7 @@ impl Utils { } pub(super) fn percent_encode(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, s: mlua::String| { + lua.create_function(|lua, s: LuaString| { let b = s.as_bytes(); match percent_encoding::percent_encode(&b, RFC_3986).into() { Cow::Borrowed(_) => Ok(s), diff --git a/yazi-proxy/src/tasks.rs b/yazi-proxy/src/tasks.rs index bc64a3df..e89200b1 100644 --- a/yazi-proxy/src/tasks.rs +++ b/yazi-proxy/src/tasks.rs @@ -1,11 +1,9 @@ -use std::ffi::OsString; - use anyhow::{Result, anyhow}; use tokio::sync::mpsc; use yazi_core::tasks::TaskOpt; use yazi_macro::{emit, relay}; -use yazi_scheduler::process::ProcessOpt; -use yazi_shared::{id::Id, url::{UrlBuf, UrlCow}}; +use yazi_scheduler::process::ShellOpt; +use yazi_shared::id::Id; pub struct TasksProxy; @@ -17,24 +15,13 @@ impl TasksProxy { rx.recv().await.ok_or_else(|| anyhow!("channel closed"))??.try_into() } - // TODO: remove - pub fn open_shell_compat(opt: ProcessOpt) { - emit!(Call(relay!(tasks:open_shell_compat).with_any("opt", opt))); + pub fn process_open(opt: ShellOpt) { + emit!(Call(relay!(tasks:process_open).with_any("opt", opt))); } - pub async fn process_exec( - cwd: UrlBuf, - cmd: OsString, - args: Vec>, - block: bool, - orphan: bool, - ) { + pub async fn process_exec(opt: ShellOpt) { let (tx, mut rx) = mpsc::unbounded_channel(); - emit!(Call( - relay!(tasks:process_open) - .with_any("opt", ProcessOpt { cwd, cmd, args, block, orphan, spread: false }) - .with_replier(tx) - )); + emit!(Call(relay!(tasks:process_open).with_any("opt", opt).with_replier(tx))); rx.recv().await; } } diff --git a/yazi-runner/src/loader/chunk.rs b/yazi-runner/src/loader/chunk.rs index 33cff2b9..9ee38aed 100644 --- a/yazi-runner/src/loader/chunk.rs +++ b/yazi-runner/src/loader/chunk.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use mlua::{AsChunk, ChunkMode}; +use mlua::chunk::{AsChunk, ChunkMode}; use yazi_shared::natsort; pub struct Chunk { diff --git a/yazi-runner/src/loader/loader.rs b/yazi-runner/src/loader/loader.rs index 6f69da1a..6d118786 100644 --- a/yazi-runner/src/loader/loader.rs +++ b/yazi-runner/src/loader/loader.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ops::Deref}; use anyhow::{Context, Result, bail, ensure}; use hashbrown::HashMap; -use mlua::{ChunkMode, ExternalError, Lua, Table}; +use mlua::{ExternalError, Lua, Table, chunk::ChunkMode}; use parking_lot::RwLock; use yazi_fs::{Xdg, provider::local::Local}; use yazi_macro::plugin_preset as preset; diff --git a/yazi-runner/src/loader/require.rs b/yazi-runner/src/loader/require.rs index 507588fc..10ad47f2 100644 --- a/yazi-runner/src/loader/require.rs +++ b/yazi-runner/src/loader/require.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, sync::Arc}; -use mlua::{ExternalResult, Function, IntoLua, Lua, MetaMethod, MultiValue, ObjectLike, Table, Value}; +use mlua::{ExternalResult, Function, IntoLua, Lua, LuaString, MetaMethod, MultiValue, ObjectLike, Table, Value}; use yazi_binding::{runtime, runtime_mut}; use super::LOADER; @@ -11,7 +11,7 @@ impl Require { pub(super) fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().raw_set( "require", - lua.create_async_function(|lua, id: mlua::String| async move { + lua.create_async_function(|lua, id: LuaString| async move { let id = id.to_str()?; let id = Self::absolute_id(&lua, &id)?; LOADER.ensure(&id, |_| ()).await.into_lua_err()?; @@ -30,7 +30,7 @@ impl Require { let mt = lua.create_table_from([ ( MetaMethod::Index.name(), - lua.create_function(move |lua, (ts, key): (Table, mlua::String)| { + lua.create_function(move |lua, (ts, key): (Table, LuaString)| { match ts.raw_get::
("__mod")?.raw_get::(&key)? { Value::Function(_) => { Self::create_wrapper(lua, id.clone(), &key.to_str()?)?.into_lua(lua) @@ -41,7 +41,7 @@ impl Require { ), ( MetaMethod::NewIndex.name(), - lua.create_function(move |_, (ts, key, value): (Table, mlua::String, Value)| { + lua.create_function(move |_, (ts, key, value): (Table, LuaString, Value)| { ts.raw_get::
("__mod")?.raw_set(key, value) })?, ), diff --git a/yazi-scheduler/src/process/in.rs b/yazi-scheduler/src/process/in.rs index 082742f1..1d1fd23d 100644 --- a/yazi-scheduler/src/process/in.rs +++ b/yazi-scheduler/src/process/in.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::OsString}; -use yazi_shared::{CompletionToken, id::Id, url::{UrlBuf, UrlCow}}; +use yazi_shared::{CompletionToken, id::Id, url::UrlBuf}; use super::ShellOpt; use crate::{TaskIn, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; @@ -46,10 +46,9 @@ impl TaskIn for ProcessIn { // --- Block #[derive(Debug)] pub(crate) struct ProcessInBlock { - pub(crate) id: Id, - pub(crate) cwd: UrlBuf, - pub(crate) cmd: OsString, - pub(crate) args: Vec>, + pub(crate) id: Id, + pub(crate) cwd: UrlBuf, + pub(crate) cmd: OsString, } impl TaskIn for ProcessInBlock { @@ -67,17 +66,16 @@ impl TaskIn for ProcessInBlock { impl From for ShellOpt { fn from(r#in: ProcessInBlock) -> Self { - Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: false, orphan: false } + Self { cwd: r#in.cwd, cmd: r#in.cmd, block: true, orphan: false } } } // --- Orphan #[derive(Debug)] pub(crate) struct ProcessInOrphan { - pub(crate) id: Id, - pub(crate) cwd: UrlBuf, - pub(crate) cmd: OsString, - pub(crate) args: Vec>, + pub(crate) id: Id, + pub(crate) cwd: UrlBuf, + pub(crate) cmd: OsString, } impl TaskIn for ProcessInOrphan { @@ -95,7 +93,7 @@ impl TaskIn for ProcessInOrphan { impl From for ShellOpt { fn from(r#in: ProcessInOrphan) -> Self { - Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: false, orphan: true } + Self { cwd: r#in.cwd, cmd: r#in.cmd, block: false, orphan: true } } } @@ -105,7 +103,6 @@ pub(crate) struct ProcessInBg { pub(crate) id: Id, pub(crate) cwd: UrlBuf, pub(crate) cmd: OsString, - pub(crate) args: Vec>, pub(crate) done: CompletionToken, } @@ -124,6 +121,6 @@ impl TaskIn for ProcessInBg { impl From for ShellOpt { fn from(r#in: ProcessInBg) -> Self { - Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: true, orphan: false } + Self { cwd: r#in.cwd, cmd: r#in.cmd, block: false, orphan: false } } } diff --git a/yazi-scheduler/src/process/mod.rs b/yazi-scheduler/src/process/mod.rs index 6bbfc5f7..7db19e8f 100644 --- a/yazi-scheduler/src/process/mod.rs +++ b/yazi-scheduler/src/process/mod.rs @@ -1,4 +1,4 @@ #[macro_use] mod macros; -yazi_macro::mod_flat!(option out process progress r#in shell); +yazi_macro::mod_flat!(out process progress r#in shell); diff --git a/yazi-scheduler/src/process/option.rs b/yazi-scheduler/src/process/option.rs deleted file mode 100644 index 6b95af14..00000000 --- a/yazi-scheduler/src/process/option.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::ffi::OsString; - -use yazi_macro::impl_data_any; -use yazi_shared::url::{UrlBuf, UrlCow}; - -// TODO: remove in favor of ShellForm -#[derive(Clone, Debug)] -pub struct ProcessOpt { - pub cwd: UrlBuf, - pub cmd: OsString, - pub args: Vec>, - pub block: bool, - pub orphan: bool, - - pub spread: bool, // TODO: remove -} - -impl_data_any!(ProcessOpt); diff --git a/yazi-scheduler/src/process/process.rs b/yazi-scheduler/src/process/process.rs index b85594bd..770c1832 100644 --- a/yazi-scheduler/src/process/process.rs +++ b/yazi-scheduler/src/process/process.rs @@ -54,14 +54,8 @@ impl Process { } pub(crate) async fn bg(&self, task: ProcessInBg) -> Result<(), ProcessOutBg> { - let mut child = super::shell(ShellOpt { - cwd: task.cwd, - cmd: task.cmd, - args: task.args, - piped: true, - orphan: false, - }) - .await?; + let mut child = + super::shell(ShellOpt { cwd: task.cwd, cmd: task.cmd, block: false, orphan: false }).await?; let done = task.done; let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines(); diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 59c580ae..2f6f95ef 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -3,25 +3,28 @@ use std::{ffi::OsString, process::Stdio}; use anyhow::Result; use tokio::process::{Child, Command}; use yazi_fs::Cwd; -use yazi_shared::url::{AsUrl, UrlBuf, UrlCow}; +use yazi_macro::impl_data_any; +use yazi_shared::url::{AsUrl, UrlBuf}; -pub(crate) struct ShellOpt { - pub(crate) cwd: UrlBuf, - pub(crate) cmd: OsString, - pub(crate) args: Vec>, - pub(crate) piped: bool, - pub(crate) orphan: bool, +#[derive(Clone, Debug)] +pub struct ShellOpt { + pub cwd: UrlBuf, + pub cmd: OsString, + pub block: bool, + pub orphan: bool, } +impl_data_any!(ShellOpt); + impl ShellOpt { #[inline] fn stdio(&self) -> Stdio { - if self.orphan { - Stdio::null() - } else if self.piped { - Stdio::piped() - } else { + if self.block { Stdio::inherit() + } else if self.orphan { + Stdio::null() + } else { + Stdio::piped() } } } @@ -32,21 +35,16 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { #[cfg(unix)] return Ok(unsafe { - use yazi_fs::FsUrl; - use yazi_shared::url::AsUrl; - Command::new("sh") .stdin(opt.stdio()) .stdout(opt.stdio()) .stderr(opt.stdio()) .arg("-c") .arg(opt.cmd) - // TODO: remove - .args(opt.args.iter().map(|u| u.as_url().unified_path_str())) .current_dir(cwd) .kill_on_drop(!opt.orphan) .pre_exec(move || { - if (opt.piped || opt.orphan) && libc::setsid() < 0 { + if !opt.block && libc::setsid() < 0 { return Err(std::io::Error::last_os_error()); } Ok(()) diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index e52376e9..b8dae4c3 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -5,7 +5,7 @@ use yazi_config::{YAZI, plugin::{FetcherArc, PreloaderArc}}; use yazi_fs::FsHash64; use yazi_shared::{CompletionToken, Throttle, id::Id, url::{UrlBuf, UrlLike}}; -use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskIn, TaskProg, Worker, fetch::FetchIn, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::PluginInEntry, preload::PreloadIn, process::{ProcessIn, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOpt}, size::SizeIn}; +use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskIn, TaskProg, Worker, fetch::FetchIn, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::PluginInEntry, preload::PreloadIn, process::{ProcessIn, ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt}, size::SizeIn}; pub struct Scheduler { pub worker: Worker, @@ -227,20 +227,14 @@ impl Scheduler { } } - pub fn process_open(&self, opt: ProcessOpt) -> CompletionToken { + pub fn process_open(&self, opt: ShellOpt) -> CompletionToken { let mut r#in: ProcessIn = if opt.block { - ProcessInBlock { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }.into() + ProcessInBlock { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd }.into() } else if opt.orphan { - ProcessInOrphan { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }.into() + ProcessInOrphan { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd }.into() } else { - ProcessInBg { - id: Id::ZERO, - cwd: opt.cwd, - cmd: opt.cmd, - args: opt.args, - done: CompletionToken::default(), - } - .into() + ProcessInBg { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd, done: CompletionToken::default() } + .into() }; let done = match &mut r#in { diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 56cc7e1a..eff058bd 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -26,7 +26,7 @@ futures = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } inventory = { workspace = true } -memchr = "2.8.2" +memchr = "2.8.3" mlua = { workspace = true } ordered-float = { workspace = true } parking_lot = { workspace = true } diff --git a/yazi-shared/src/any_data.rs b/yazi-shared/src/any_data.rs index ecd4208e..a5204bf8 100644 --- a/yazi-shared/src/any_data.rs +++ b/yazi-shared/src/any_data.rs @@ -2,7 +2,5 @@ use mlua::UserData; use crate::data::DataAny; -#[derive(Debug)] +#[derive(Debug, UserData)] pub struct AnyData(pub Box); - -impl UserData for AnyData {} diff --git a/yazi-shared/src/id/lua.rs b/yazi-shared/src/id/lua.rs index 15b68077..30a7e9c3 100644 --- a/yazi-shared/src/id/lua.rs +++ b/yazi-shared/src/id/lua.rs @@ -1,6 +1,6 @@ -use mlua::{ExternalError, ExternalResult, FromLua, Lua, UserData, UserDataFields, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; -use crate::id::Id; +use crate::{LOG_LEVEL, id::Id}; impl FromLua for Id { fn from_lua(value: Value, _: &Lua) -> mlua::Result { @@ -16,4 +16,12 @@ impl UserData for Id { fn add_fields>(fields: &mut F) { fields.add_field_method_get("value", |_, me| Ok(me.get())); } + + fn add_methods>(methods: &mut M) { + if !LOG_LEVEL.get().is_none() { + methods.add_meta_function(MetaMethod::ToDebugString, |_, ud: AnyUserData| { + Ok(format!("Id({:?}): {}", ud.to_pointer(), *ud.borrow::()?)) + }); + } + } } diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index 53ef3f91..bb6c1227 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalError, ExternalResult, FromLua, MetaMethod, UserData, UserDataMethods, Value}; +use mlua::{ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataMethods, Value}; use serde::Deserialize; use strum::{Display, EnumString, FromRepr, IntoStaticStr}; use yazi_shim::strum::IntoStr; @@ -41,7 +41,7 @@ impl Layer { } impl FromLua for Layer { - fn from_lua(value: Value, _: &mlua::Lua) -> mlua::Result { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(match value { Value::String(s) => s.to_str()?.parse().into_lua_err()?, Value::UserData(ud) => *ud.borrow::()?, diff --git a/yazi-shared/src/path/lua.rs b/yazi-shared/src/path/lua.rs index a087470e..c53d9ffe 100644 --- a/yazi-shared/src/path/lua.rs +++ b/yazi-shared/src/path/lua.rs @@ -1,7 +1,7 @@ -use mlua::{ExternalError, ExternalResult, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_shim::mlua::UserDataFieldsExt; -use crate::{path::{PathBufDyn, PathLike, StripPrefixError}, strand::{AsStrand, StrandCow}}; +use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, strand::{AsStrand, StrandCow}}; pub type PathRef = UserDataRef; @@ -11,7 +11,7 @@ impl PathBufDyn { "Path", lua.create_table_from([( "os", - lua.create_function(|_, s: mlua::String| { + lua.create_function(|_, s: LuaString| { Ok(Self::from(s.as_bytes().as_strand().as_os_path().into_lua_err()?)) })?, )])?, @@ -89,11 +89,17 @@ impl UserData for PathBufDyn { methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); - methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { + methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: LuaString| { lua.create_external_string([lhs.encoded_bytes(), &rhs.as_bytes()].concat()) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: PathRef| Ok(*me == *other)); methods .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); + + if !LOG_LEVEL.get().is_none() { + methods.add_meta_function(MetaMethod::ToDebugString, |_, ud: AnyUserData| { + Ok(format!("Path({:?}): {:?}", ud.to_pointer(), *ud.borrow::()?)) + }); + } } } diff --git a/yazi-shared/src/url/lua.rs b/yazi-shared/src/url/lua.rs index bd8e787a..559a5fff 100644 --- a/yazi-shared/src/url/lua.rs +++ b/yazi-shared/src/url/lua.rs @@ -1,7 +1,7 @@ -use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; use yazi_shim::mlua::UserDataFieldsExt; -use crate::{path::{PathBufDyn, PathLike, StripPrefixError}, scheme::{SchemeCow, SchemeLike}, strand::{StrandLike, ToStrand}, url::{UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; +use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, scheme::{SchemeCow, SchemeLike}, strand::{StrandLike, ToStrand}, url::{UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; pub type UrlRef = UserDataRef; @@ -124,17 +124,23 @@ impl UserData for UrlBuf { methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); - methods.add_function("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { - ud.take::()?.into_search(domain.to_str()?).into_lua_err() + methods.add_method_once("into_search", |_, me, domain: LuaString| { + me.into_search(domain.to_str()?).into_lua_err() }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other)); methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { lua.create_string(me.to_strand().encoded_bytes()) }); - methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { + methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: LuaString| { lua.create_external_string([lhs.to_strand().encoded_bytes(), &rhs.as_bytes()].concat()) }); + + if !LOG_LEVEL.get().is_none() { + methods.add_meta_function(MetaMethod::ToDebugString, |_, ud: AnyUserData| { + Ok(format!("Url({:?}): {:?}", ud.to_pointer(), *ud.borrow::()?)) + }); + } } fn register(registry: &mut UserDataRegistry) { diff --git a/yazi-shim/src/mlua/ser.rs b/yazi-shim/src/mlua/ser.rs index f7c55eb2..ebddd5db 100644 --- a/yazi-shim/src/mlua/ser.rs +++ b/yazi-shim/src/mlua/ser.rs @@ -1,4 +1,4 @@ -use mlua::SerializeOptions; +use mlua::serde::SerializeOptions; pub const SER_OPT: SerializeOptions = SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); diff --git a/yazi-shim/src/mlua/string.rs b/yazi-shim/src/mlua/string.rs index 176ecb79..866bc44a 100644 --- a/yazi-shim/src/mlua/string.rs +++ b/yazi-shim/src/mlua/string.rs @@ -1,12 +1,12 @@ use std::{fmt, ops::Deref}; -use mlua::{FromLua, IntoLua, Lua, Value}; +use mlua::{FromLua, IntoLua, Lua, LuaString, Value}; #[derive(Clone)] -pub struct ByteString(mlua::String); +pub struct ByteString(LuaString); impl Deref for ByteString { - type Target = mlua::String; + type Target = LuaString; fn deref(&self) -> &Self::Target { &self.0 } } @@ -17,7 +17,7 @@ impl fmt::Display for ByteString { impl FromLua for ByteString { fn from_lua(value: Value, lua: &Lua) -> mlua::Result { - Ok(Self(mlua::String::from_lua(value, lua)?)) + Ok(Self(LuaString::from_lua(value, lua)?)) } } diff --git a/yazi-term/Cargo.toml b/yazi-term/Cargo.toml index a41e5553..42a01089 100644 --- a/yazi-term/Cargo.toml +++ b/yazi-term/Cargo.toml @@ -21,6 +21,7 @@ yazi-tty = { path = "../yazi-tty", version = "26.5.9" } anyhow = { workspace = true } base64 = { workspace = true } bitflags = { workspace = true } +compact_str = { workspace = true } futures = { workspace = true } mlua = { workspace = true } parking_lot = { workspace = true } diff --git a/yazi-term/src/event/keyboard.rs b/yazi-term/src/event/keyboard.rs index b94130da..6952eeb8 100644 --- a/yazi-term/src/event/keyboard.rs +++ b/yazi-term/src/event/keyboard.rs @@ -1,41 +1,39 @@ use bitflags::bitflags; +use compact_str::CompactString; use serde::{Deserialize, Serialize}; use crate::{ParseError, Result, bail, event::Modifiers}; -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)] pub struct KeyEvent { #[serde(flatten)] pub code: KeyCode, pub kind: KeyEventKind, pub modifiers: Modifiers, pub state: KeyEventState, + #[serde(skip_serializing_if = "CompactString::is_empty")] + pub text: CompactString, } impl KeyEvent { - pub const fn new(code: KeyCode, modifiers: Modifiers) -> Self { - Self { code, kind: KeyEventKind::Press, modifiers, state: KeyEventState::empty() } + pub fn new(code: KeyCode, modifiers: Modifiers) -> Self { + Self { code, modifiers, ..Default::default() } } - pub fn plain(&self) -> Option { + pub fn text<'a>(&'a self, buf: &'a mut [u8; 4]) -> Option<&'a str> { use Modifiers as M; match self.code { - KeyCode::Char(c) if !self.modifiers.intersects(M::CONTROL | M::ALT | M::SUPER) => Some(c), + _ if self.modifiers.intersects(M::CONTROL | M::ALT | M::SUPER) => None, + KeyCode::Char(c) if self.text.is_empty() => Some(c.encode_utf8(buf)), + KeyCode::Char(_) | KeyCode::Null if !self.text.is_empty() => Some(&self.text), _ => None, } } } impl From for KeyEvent { - fn from(code: KeyCode) -> Self { - Self { - code, - kind: KeyEventKind::Press, - modifiers: Modifiers::empty(), - state: KeyEventState::empty(), - } - } + fn from(code: KeyCode) -> Self { Self { code, ..Default::default() } } } // --- Kind @@ -116,6 +114,8 @@ pub enum KeyCode { } impl KeyCode { + pub fn implies_shift(self) -> bool { matches!(self, Self::Char(c) if c.is_uppercase()) } + pub(crate) fn from_xterm_modifier(r#final: u8) -> Result { Ok(match r#final { b'A' => Self::Up, @@ -138,6 +138,7 @@ impl KeyCode { } let code = match char::from_u32(codepoint).ok_or(ParseError::Invalid)? { + '\0' => Self::Null, '\x1B' => Self::Escape, '\r' => Self::Enter, '\t' => Self::Tab, diff --git a/yazi-term/src/parser/csi.rs b/yazi-term/src/parser/csi.rs index 82a40b1a..b5b733fa 100644 --- a/yazi-term/src/parser/csi.rs +++ b/yazi-term/src/parser/csi.rs @@ -1,5 +1,7 @@ use std::str::{self, FromStr}; +use compact_str::CompactString; + use super::parser::Parser; use crate::{ParseError, Result, bail, event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, Modifiers, MouseEvent, MouseEventKind}}; @@ -84,8 +86,10 @@ impl Parser { let (mut code, state_from_keycode) = KeyCode::from_codepoint(parse_next(&mut codepoints)?)?; let (mut modifiers, kind, state_from_modifiers) = parse_mks(&mut it).unwrap_or_default(); + let text = parse_text(it.next().unwrap_or_default())?; if let KeyCode::Modifier(c) = code + && kind != KeyEventKind::Release && let Some(m) = c.to_modifier() { modifiers |= m; @@ -97,9 +101,10 @@ impl Parser { // which contains the shifted character according to the keyboard layout. if modifiers.contains(Modifiers::SHIFT) && let Ok(Some(shifted)) = parse_next(&mut codepoints).map(char::from_u32) + && code != KeyCode::Char(shifted) { code = KeyCode::Char(shifted); - modifiers.remove(Modifiers::SHIFT); + modifiers.set(Modifiers::SHIFT, code.implies_shift()); } Ok(Event::Key(KeyEvent { @@ -107,6 +112,7 @@ impl Parser { modifiers, kind, state: state_from_keycode | state_from_modifiers, + text, })) } @@ -123,7 +129,7 @@ impl Parser { let (modifiers, kind, _) = parse_mks(&mut it).unwrap_or_default(); let code = KeyCode::from_xterm_modifier(seq[seq.len() - 1])?; - Ok(Event::Key(KeyEvent { code, modifiers, kind, state: KeyEventState::empty() })) + Ok(Event::Key(KeyEvent { code, modifiers, kind, ..Default::default() })) } /// Parses legacy `CSI modifier final` - no semicolon, modifier digit @@ -138,10 +144,9 @@ impl Parser { } Ok(Event::Key(KeyEvent { - code: KeyCode::from_xterm_modifier(seq[seq.len() - 1])?, + code: KeyCode::from_xterm_modifier(seq[seq.len() - 1])?, modifiers: Modifiers::from_vt_mask(modifier - b'0'), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), + ..Default::default() })) } @@ -173,7 +178,7 @@ impl Parser { _ => bail!(), }; - let event = Event::Key(KeyEvent { code, modifiers, kind, state }); + let event = Event::Key(KeyEvent { code, modifiers, kind, state, ..Default::default() }); Ok(event) } @@ -277,3 +282,13 @@ where KeyEventState::from_vt_mask(mask), )) } + +fn parse_text(s: &str) -> Result { + if s.is_empty() { + return Ok(CompactString::default()); + } + + s.split(':') + .map(|codepoint| char::from_u32(codepoint.parse()?).ok_or(ParseError::Invalid)) + .collect() +} diff --git a/yazi-tty/src/sequence/query.rs b/yazi-tty/src/sequence/query.rs index f43ba3f7..34785a20 100644 --- a/yazi-tty/src/sequence/query.rs +++ b/yazi-tty/src/sequence/query.rs @@ -51,13 +51,6 @@ impl Display for RequestCursorBlink { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?12$p") } } -/// Request keyboard enhancement flags (CSI u query) -pub struct RequestKeyboardFlags; - -impl Display for RequestKeyboardFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?u") } -} - /// Device Status Report (DSR) pub struct RequestDeviceStatus; diff --git a/yazi-tui/src/raterm.rs b/yazi-tui/src/raterm.rs index 24b8a259..4908d59c 100644 --- a/yazi-tui/src/raterm.rs +++ b/yazi-tui/src/raterm.rs @@ -1,4 +1,4 @@ -use std::{io::{self, Write}, ops::Deref}; +use std::{io, ops::Deref}; use anyhow::Result; use ratatui_core::{buffer::Buffer, layout::Rect, terminal::{CompletedFrame, Frame, Terminal}}; @@ -8,7 +8,7 @@ use yazi_macro::writef; use yazi_proxy::AppProxy; use yazi_shim::cell::SyncCell; use yazi_term::{TERM, event::{Event, KeyEventKind}, stream::EventStream}; -use yazi_tty::{TTY, TtyWriter, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RequestKeyboardFlags, RestoreCursorStyle, SetTitle, ShowCursor}}; +use yazi_tty::{TTY, TtyWriter, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RestoreCursorStyle, SetTitle, ShowCursor}}; use crate::{RatermBackend, RatermOption, RatermState}; @@ -43,9 +43,12 @@ impl Raterm { let opt = RatermOption::default(); writef!( TTY.writer(), - "{}{RequestCursorStyle}{RequestCursorBlink}{RequestKeyboardFlags}{RequestDA1}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}", + "{}{RequestCursorStyle}{RequestCursorBlink}{RequestDA1}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}{}", If(!TMUX.get(), EnterAlternateScreen), If(TMUX.get(), EnterAlternateScreen), + PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES + | PushKeyboardFlags::REPORT_ALTERNATE_KEYS + | PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES, EnableDrag(""), EnableDrop(&["text/uri-list"]), If(opt.mouse, EnableMouseCapture), @@ -55,14 +58,6 @@ impl Raterm { Mux::tmux_drain()?; STATE.set(RatermState::new(&resp, &opt)); - if STATE.get().csi_u { - write!( - TTY.writer(), - "{}", - PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES | PushKeyboardFlags::REPORT_ALTERNATE_KEYS, - )?; - } - let mut term = Self { inner: Terminal::new(RatermBackend::new(TTY.writer()))?, stream: EventStream::from(&*TERM), @@ -82,9 +77,8 @@ impl Raterm { _ = writef!( TTY.writer(), - "{}{DisableDrop}{DisableDrag}{}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", + "{}{PopKeyboardFlags}{DisableDrop}{DisableDrag}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", If(state.mouse, DisableMouseCapture), - If(state.csi_u, PopKeyboardFlags), RestoreCursorStyle { shape: state.cursor_shape, blink: state.cursor_blink }, If(state.title, SetTitle("")), ); @@ -100,7 +94,7 @@ impl Raterm { loop { match rx.recv().await { Some(Ok(event)) => match event { - Event::Key(key) if key.kind != KeyEventKind::Press => continue, + Event::Key(key) if key.kind == KeyEventKind::Release => continue, Event::Mouse(mouse) if !YAZI.mgr.mouse_events.get().contains(mouse.kind.into()) => { continue; } diff --git a/yazi-tui/src/state.rs b/yazi-tui/src/state.rs index 645e3c43..445ab719 100644 --- a/yazi-tui/src/state.rs +++ b/yazi-tui/src/state.rs @@ -2,7 +2,6 @@ use crate::RatermOption; #[derive(Clone, Copy)] pub struct RatermState { - pub csi_u: bool, pub mouse: bool, pub title: bool, pub cursor_shape: u8, @@ -11,18 +10,10 @@ pub struct RatermState { impl RatermState { pub(super) const fn default() -> Self { - Self { - csi_u: false, - mouse: false, - title: false, - cursor_shape: 0, - cursor_blink: false, - } + Self { mouse: false, title: false, cursor_shape: 0, cursor_blink: false } } pub(super) fn new(resp: &str, opt: &RatermOption) -> Self { - let csi_u = resp.contains("\x1b[?0u"); - let cursor_shape = resp .split_once("\x1bP1$r") .and_then(|(_, s)| s.bytes().next()) @@ -31,6 +22,6 @@ impl RatermState { let cursor_blink = resp.contains("\x1b[?12;1$y"); - Self { csi_u, mouse: opt.mouse, title: false, cursor_shape, cursor_blink } + Self { mouse: opt.mouse, title: false, cursor_shape, cursor_blink } } } diff --git a/yazi-vfs/src/provider/lua.rs b/yazi-vfs/src/provider/lua.rs index 5e86b21e..e61ba9bd 100644 --- a/yazi-vfs/src/provider/lua.rs +++ b/yazi-vfs/src/provider/lua.rs @@ -1,4 +1,4 @@ -use mlua::{IntoLuaMulti, UserData, UserDataMethods, Value}; +use mlua::{IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use yazi_binding::Error; @@ -22,7 +22,7 @@ impl UserData for RwFile { Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); - methods.add_async_method_mut("write_all", |lua, mut me, src: mlua::String| async move { + methods.add_async_method_mut("write_all", |lua, mut me, src: LuaString| async move { match me.write_all(&src.as_bytes()).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), diff --git a/yazi-widgets/src/input/actor/type.rs b/yazi-widgets/src/input/actor/type.rs index 2de0332c..950b492a 100644 --- a/yazi-widgets/src/input/actor/type.rs +++ b/yazi-widgets/src/input/actor/type.rs @@ -6,14 +6,15 @@ use yazi_term::event::KeyEvent; use crate::input::{Input, InputMode}; impl Input { - pub fn r#type(&mut self, key: KeyEvent) -> Result { - let Some(c) = key.plain() else { return Ok(false) }; + pub fn r#type(&mut self, key: &KeyEvent) -> Result { + let mut buf = [0; 4]; + let Some(text) = key.text(&mut buf) else { return Ok(false) }; if self.mode() == InputMode::Insert { - self.type_str(c.encode_utf8(&mut [0; 4]))?; + self.type_str(text)?; return Ok(true); } else if self.mode() == InputMode::Replace { - self.replace_str(c.encode_utf8(&mut [0; 4]))?; + self.replace_str(text)?; return Ok(true); } From 6d84921e7004eb8d49ba13a4acc97c6cfeb094b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 13 Jul 2026 02:01:37 +0800 Subject: [PATCH 03/12] feat!: custom VFS provider (#4118) --- CHANGELOG.md | 5 + Cargo.lock | 7 +- yazi-actor/src/app/quit.rs | 2 +- yazi-actor/src/cmp/trigger.rs | 37 ++- yazi-actor/src/mgr/bulk_create.rs | 12 +- yazi-actor/src/mgr/bulk_rename.rs | 10 +- yazi-actor/src/mgr/cd.rs | 4 +- yazi-actor/src/mgr/create.rs | 18 +- yazi-actor/src/mgr/displace.rs | 4 +- yazi-actor/src/mgr/download.rs | 4 +- yazi-actor/src/mgr/refresh.rs | 7 +- yazi-actor/src/mgr/rename.rs | 12 +- yazi-actor/src/mgr/stash.rs | 2 +- yazi-adapter/src/image.rs | 2 +- yazi-binding/src/chan.rs | 17 +- yazi-binding/src/error.rs | 13 +- yazi-boot/src/boot.rs | 6 +- 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 | 4 +- yazi-codegen/src/lib.rs | 4 +- yazi-config/src/lib.rs | 8 +- yazi-config/src/pattern.rs | 52 ++- yazi-config/src/popup/input.rs | 4 +- yazi-config/src/preset.rs | 6 +- yazi-config/src/tests.rs | 9 + yazi-config/src/theme/custom.rs | 6 +- yazi-config/src/vfs/lua.rs | 17 + yazi-config/src/vfs/mod.rs | 1 + yazi-config/src/vfs/service.rs | 64 ++++ yazi-config/src/vfs/services.rs | 47 +++ .../config => yazi-config/src/vfs}/sftp.rs | 13 +- yazi-config/src/vfs/vfs.rs | 53 +++ yazi-core/src/mgr/snap.rs | 2 +- yazi-core/src/tab/preview.rs | 6 +- yazi-dds/src/state.rs | 9 +- yazi-dds/src/stream.rs | 2 +- yazi-fm/src/main.rs | 8 +- yazi-fs/src/cha/mode.rs | 5 + yazi-fs/src/{provider => engine}/attrs.rs | 17 + yazi-fs/src/engine/capabilities.rs | 22 ++ yazi-fs/src/engine/demand.rs | 55 +++ .../{provider => engine}/local/absolute.rs | 6 +- .../{provider => engine}/local/calculator.rs | 0 .../{provider => engine}/local/casefold.rs | 0 .../src/{provider => engine}/local/copier.rs | 4 +- .../local/gate.rs => engine/local/demand.rs} | 6 +- .../{provider => engine}/local/dir_entry.rs | 2 +- .../{provider => engine}/local/identical.rs | 0 .../src/{provider => engine}/local/local.rs | 80 +++-- yazi-fs/src/{provider => engine}/local/mod.rs | 2 +- .../{provider => engine}/local/read_dir.rs | 2 +- yazi-fs/src/engine/mod.rs | 3 + yazi-fs/src/{provider => engine}/traits.rs | 34 +- yazi-fs/src/file/files.rs | 7 + yazi-fs/src/fns.rs | 4 +- yazi-fs/src/lib.rs | 4 +- yazi-fs/src/path/clean.rs | 34 +- yazi-fs/src/path/expand.rs | 93 +++--- yazi-fs/src/path/normalize.rs | 2 +- yazi-fs/src/provider/capabilities.rs | 4 - yazi-fs/src/provider/mod.rs | 3 - yazi-fs/src/scheme.rs | 37 --- yazi-fs/src/spec.rs | 33 ++ yazi-fs/src/url.rs | 31 +- yazi-parser/src/mgr/cd.rs | 4 +- yazi-parser/src/mgr/reveal.rs | 4 +- yazi-parser/src/mgr/tab_create.rs | 4 +- yazi-plugin/preset/components/header.lua | 4 +- yazi-plugin/preset/plugins/fzf.lua | 2 +- yazi-plugin/preset/plugins/mime-dir.lua | 2 +- yazi-plugin/preset/plugins/mime-remote.lua | 2 +- yazi-plugin/src/fs/calculator.rs | 4 +- yazi-plugin/src/fs/fs.rs | 34 +- yazi-plugin/src/ui/ui.rs | 11 +- yazi-plugin/src/utils/app.rs | 2 +- yazi-plugin/src/utils/sync.rs | 2 +- yazi-runner/Cargo.toml | 1 + yazi-runner/src/lib.rs | 3 +- yazi-runner/src/loader/loader.rs | 2 +- yazi-runner/src/provider/job.rs | 162 +++++++++ yazi-runner/src/provider/mod.rs | 1 + yazi-runner/src/provider/provider.rs | 42 +++ yazi-runner/src/provider/result.rs | 43 +++ yazi-scheduler/src/file/file.rs | 50 +-- yazi-scheduler/src/file/in.rs | 2 +- yazi-scheduler/src/file/transaction.rs | 10 +- yazi-scheduler/src/file/traverse.rs | 6 +- yazi-scheduler/src/hook/hook.rs | 6 +- yazi-scheduler/src/scheduler.rs | 4 +- yazi-scheduler/src/size/size.rs | 4 +- yazi-shared/src/auth/auth.rs | 65 ++++ yazi-shared/src/auth/encode.rs | 26 ++ yazi-shared/src/auth/inventory.rs | 9 + yazi-shared/src/auth/kind.rs | 42 +++ yazi-shared/src/auth/mod.rs | 3 + yazi-shared/src/auth/scheme.rs | 63 ++++ ...bab_cased_string.rs => kebab_cased_key.rs} | 34 +- yazi-shared/src/lib.rs | 5 +- yazi-shared/src/loc/buf.rs | 24 +- yazi-shared/src/loc/loc.rs | 13 +- yazi-shared/src/path/kind.rs | 15 +- yazi-shared/src/pool/mod.rs | 2 +- yazi-shared/src/scheme/cow.rs | 232 ------------- yazi-shared/src/scheme/inventory.rs | 9 - yazi-shared/src/scheme/kind.rs | 84 ----- yazi-shared/src/scheme/lua.rs | 20 -- yazi-shared/src/scheme/mod.rs | 1 - yazi-shared/src/scheme/ref.rs | 95 ------ yazi-shared/src/scheme/scheme.rs | 47 --- yazi-shared/src/scheme/traits.rs | 65 ---- yazi-shared/src/{scheme => spec}/encode.rs | 27 +- yazi-shared/src/spec/inventory.rs | 9 + yazi-shared/src/spec/lua.rs | 23 ++ yazi-shared/src/spec/mod.rs | 1 + yazi-shared/src/spec/parsed.rs | 43 +++ yazi-shared/src/spec/spec.rs | 129 ++++++++ yazi-shared/src/strand/kind.rs | 15 +- yazi-shared/src/tests.rs | 23 +- yazi-shared/src/url/buf.rs | 90 +++-- yazi-shared/src/url/component.rs | 8 +- yazi-shared/src/url/components.rs | 72 ++-- yazi-shared/src/url/cov.rs | 2 +- yazi-shared/src/url/cow.rs | 206 +++++------- yazi-shared/src/url/de.rs | 23 +- yazi-shared/src/url/display.rs | 2 +- yazi-shared/src/url/encode.rs | 17 +- yazi-shared/src/url/like.rs | 12 +- yazi-shared/src/url/lua.rs | 18 +- yazi-shared/src/url/traits.rs | 14 +- yazi-shared/src/url/url.rs | 313 ++++++++---------- yazi-shim/src/fs/error.rs | 10 + yazi-vfs/Cargo.toml | 5 +- yazi-vfs/src/cha.rs | 6 +- yazi-vfs/src/config/mod.rs | 1 - yazi-vfs/src/config/service.rs | 19 -- yazi-vfs/src/config/services.rs | 48 --- yazi-vfs/src/config/vfs.rs | 52 --- .../src/{provider/sftp => engine}/absolute.rs | 9 +- .../src/{provider => engine}/calculator.rs | 2 +- yazi-vfs/src/{provider => engine}/copier.rs | 18 +- .../{provider/gate.rs => engine/demand.rs} | 68 +--- .../src/{provider => engine}/dir_entry.rs | 10 +- .../provider.rs => engine/engine.rs} | 89 +++-- .../providers.rs => engine/engines.rs} | 66 ++-- yazi-vfs/src/engine/lua/demand.rs | 61 ++++ yazi-vfs/src/engine/lua/file.rs | 197 +++++++++++ yazi-vfs/src/engine/lua/lua.rs | 205 ++++++++++++ yazi-vfs/src/engine/lua/mod.rs | 1 + yazi-vfs/src/engine/lua/read_dir.rs | 33 ++ yazi-vfs/src/engine/mod.rs | 5 + yazi-vfs/src/{provider => engine}/read_dir.rs | 6 +- yazi-vfs/src/{provider => engine}/rw_file.rs | 49 ++- .../src/{provider => engine}/sftp/conn.rs | 8 +- .../sftp/gate.rs => engine/sftp/demand.rs} | 52 ++- .../src/{provider => engine}/sftp/metadata.rs | 2 +- yazi-vfs/src/engine/sftp/mod.rs | 12 + .../src/{provider => engine}/sftp/read_dir.rs | 2 +- .../src/{provider => engine}/sftp/sftp.rs | 68 ++-- yazi-vfs/src/entries.rs | 8 +- yazi-vfs/src/file.rs | 8 +- yazi-vfs/src/fns.rs | 15 +- yazi-vfs/src/lib.rs | 4 +- yazi-vfs/src/provider/lua.rs | 32 -- yazi-vfs/src/provider/mod.rs | 5 - yazi-vfs/src/provider/sftp/mod.rs | 9 - yazi-watcher/src/local/local.rs | 6 +- yazi-watcher/src/reporter.rs | 8 +- yazi-watcher/src/watched.rs | 20 +- yazi-widgets/Cargo.toml | 1 - yazi-widgets/src/input/input_arc.rs | 10 +- 173 files changed, 2665 insertions(+), 1852 deletions(-) create mode 100644 yazi-config/src/tests.rs create mode 100644 yazi-config/src/vfs/lua.rs create mode 100644 yazi-config/src/vfs/mod.rs create mode 100644 yazi-config/src/vfs/service.rs create mode 100644 yazi-config/src/vfs/services.rs rename {yazi-vfs/src/config => yazi-config/src/vfs}/sftp.rs (80%) create mode 100644 yazi-config/src/vfs/vfs.rs rename yazi-fs/src/{provider => engine}/attrs.rs (75%) create mode 100644 yazi-fs/src/engine/capabilities.rs create mode 100644 yazi-fs/src/engine/demand.rs rename yazi-fs/src/{provider => engine}/local/absolute.rs (86%) rename yazi-fs/src/{provider => engine}/local/calculator.rs (100%) rename yazi-fs/src/{provider => engine}/local/casefold.rs (100%) rename yazi-fs/src/{provider => engine}/local/copier.rs (97%) rename yazi-fs/src/{provider/local/gate.rs => engine/local/demand.rs} (90%) rename yazi-fs/src/{provider => engine}/local/dir_entry.rs (95%) rename yazi-fs/src/{provider => engine}/local/identical.rs (100%) rename yazi-fs/src/{provider => engine}/local/local.rs (74%) rename yazi-fs/src/{provider => engine}/local/mod.rs (62%) rename yazi-fs/src/{provider => engine}/local/read_dir.rs (94%) create mode 100644 yazi-fs/src/engine/mod.rs rename yazi-fs/src/{provider => engine}/traits.rs (86%) delete mode 100644 yazi-fs/src/provider/capabilities.rs delete mode 100644 yazi-fs/src/provider/mod.rs delete mode 100644 yazi-fs/src/scheme.rs create mode 100644 yazi-fs/src/spec.rs create mode 100644 yazi-runner/src/provider/job.rs create mode 100644 yazi-runner/src/provider/mod.rs create mode 100644 yazi-runner/src/provider/provider.rs create mode 100644 yazi-runner/src/provider/result.rs create mode 100644 yazi-shared/src/auth/auth.rs create mode 100644 yazi-shared/src/auth/encode.rs create mode 100644 yazi-shared/src/auth/inventory.rs create mode 100644 yazi-shared/src/auth/kind.rs create mode 100644 yazi-shared/src/auth/mod.rs create mode 100644 yazi-shared/src/auth/scheme.rs rename yazi-shared/src/{kebab_cased_string.rs => kebab_cased_key.rs} (57%) delete mode 100644 yazi-shared/src/scheme/cow.rs delete mode 100644 yazi-shared/src/scheme/inventory.rs delete mode 100644 yazi-shared/src/scheme/kind.rs delete mode 100644 yazi-shared/src/scheme/lua.rs delete mode 100644 yazi-shared/src/scheme/mod.rs delete mode 100644 yazi-shared/src/scheme/ref.rs delete mode 100644 yazi-shared/src/scheme/scheme.rs delete mode 100644 yazi-shared/src/scheme/traits.rs rename yazi-shared/src/{scheme => spec}/encode.rs (58%) create mode 100644 yazi-shared/src/spec/inventory.rs create mode 100644 yazi-shared/src/spec/lua.rs create mode 100644 yazi-shared/src/spec/mod.rs create mode 100644 yazi-shared/src/spec/parsed.rs create mode 100644 yazi-shared/src/spec/spec.rs delete mode 100644 yazi-vfs/src/config/mod.rs delete mode 100644 yazi-vfs/src/config/service.rs delete mode 100644 yazi-vfs/src/config/services.rs delete mode 100644 yazi-vfs/src/config/vfs.rs rename yazi-vfs/src/{provider/sftp => engine}/absolute.rs (53%) rename yazi-vfs/src/{provider => engine}/calculator.rs (96%) rename yazi-vfs/src/{provider => engine}/copier.rs (90%) rename yazi-vfs/src/{provider/gate.rs => engine/demand.rs} (59%) rename yazi-vfs/src/{provider => engine}/dir_entry.rs (73%) rename yazi-vfs/src/{provider/provider.rs => engine/engine.rs} (59%) rename yazi-vfs/src/{provider/providers.rs => engine/engines.rs} (68%) create mode 100644 yazi-vfs/src/engine/lua/demand.rs create mode 100644 yazi-vfs/src/engine/lua/file.rs create mode 100644 yazi-vfs/src/engine/lua/lua.rs create mode 100644 yazi-vfs/src/engine/lua/mod.rs create mode 100644 yazi-vfs/src/engine/lua/read_dir.rs create mode 100644 yazi-vfs/src/engine/mod.rs rename yazi-vfs/src/{provider => engine}/read_dir.rs (68%) rename yazi-vfs/src/{provider => engine}/rw_file.rs (67%) rename yazi-vfs/src/{provider => engine}/sftp/conn.rs (98%) rename yazi-vfs/src/{provider/sftp/gate.rs => engine/sftp/demand.rs} (55%) rename yazi-vfs/src/{provider => engine}/sftp/metadata.rs (96%) create mode 100644 yazi-vfs/src/engine/sftp/mod.rs rename yazi-vfs/src/{provider => engine}/sftp/read_dir.rs (95%) rename yazi-vfs/src/{provider => engine}/sftp/sftp.rs (75%) delete mode 100644 yazi-vfs/src/provider/lua.rs delete mode 100644 yazi-vfs/src/provider/mod.rs delete mode 100644 yazi-vfs/src/provider/sftp/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e56815..deded357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Make help menu a command palette ([#4074]) - Input history ([#4104]) - Experimental `%y`, `%Y`, `%t`, `%T`, `%yN`, `%YN`, `%tN`, `%TN` shell formatting parameters ([#4108]) +- Custom VFS provider ([#4118]) - Make visual mode support wraparound scrolling ([#4101]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - Context-aware icons for inputs ([#4080]) @@ -31,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Changed - Rename `` to `` ([#3989]) +- Rename `type` field in `vfs.toml` to `kind`, leaving `type` available for future custom VFS parameters ([#4118]) +- Remove `Url.is_archive` so `archive://` can be registered as a custom scheme ([#4118]) - Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096]) - Remove `help:filter` action since the filter input is now always available ([#4074]) - `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074]) @@ -40,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Deprecate `backward --far` and `forward --far` in favor of `backward wide` and `forward wide`, respectively ([#4012]) - Deprecate `tab::Mode.is_visual` in favor of the new `tab::Mode.is_normal` ([#4101]) +- Deprecate `Url.is_regular`, `Url.is_search`, and `Url.domain` in favor of `Url.spec.is_regular`, `Url.spec.is_search`, and `Url.spec.domain`, respectively ([#4118]) ### Fixed @@ -1775,3 +1779,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4101]: https://github.com/sxyazi/yazi/pull/4101 [#4104]: https://github.com/sxyazi/yazi/pull/4104 [#4108]: https://github.com/sxyazi/yazi/pull/4108 +[#4118]: https://github.com/sxyazi/yazi/pull/4118 diff --git a/Cargo.lock b/Cargo.lock index dba80645..5ba1d704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5197,6 +5197,7 @@ dependencies = [ "hashbrown 0.17.1", "mlua", "parking_lot", + "strum", "thiserror 2.0.18", "tokio", "tokio-util", @@ -5381,15 +5382,14 @@ dependencies = [ "mlua", "parking_lot", "russh", - "serde", "tokio", - "toml", "tracing", "typed-path", "yazi-binding", - "yazi-codegen", + "yazi-config", "yazi-fs", "yazi-macro", + "yazi-runner", "yazi-sftp", "yazi-shared", "yazi-shim", @@ -5436,7 +5436,6 @@ dependencies = [ "tracing", "unicode-width", "yazi-binding", - "yazi-dds", "yazi-macro", "yazi-shared", "yazi-shim", diff --git a/yazi-actor/src/app/quit.rs b/yazi-actor/src/app/quit.rs index 2c81556c..429e95c5 100644 --- a/yazi-actor/src/app/quit.rs +++ b/yazi-actor/src/app/quit.rs @@ -2,7 +2,7 @@ use std::process; use anyhow::Result; use yazi_boot::ARGS; -use yazi_fs::provider::{Provider, local::Local}; +use yazi_fs::engine::{Engine, local::Local}; use yazi_parser::app::QuitForm; use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}}; use yazi_tui::Raterm; diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index bb4a631e..f53c8cba 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -2,12 +2,12 @@ use std::{io, mem}; use anyhow::Result; use yazi_core::cmp::{CmpItem, CmpOpt}; -use yazi_fs::{path::clean_url, provider::{DirReader, FileHolder}}; +use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::TriggerForm; use yazi_proxy::CmpProxy; -use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; -use yazi_vfs::provider; +use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_vfs::engine; use crate::{Actor, Ctx}; @@ -36,7 +36,7 @@ impl Actor for Trigger { } cx.cmp.handle = Some(tokio::spawn(async move { - let mut dir = provider::read_dir(&parent).await?; + let mut dir = engine::read_dir(&parent).await?; let mut cache = vec![]; // "/" is both a directory separator and the root directory per se @@ -66,14 +66,14 @@ impl Actor for Trigger { impl Trigger { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { - let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; + let (spec, path) = Spec::parse(s.as_bytes()).ok()?; if path.is_empty() && !AnyAsciiChar::SEP.predicate(s.bytes().last()?) { return None; // We don't complete a `sftp://test`, but `sftp://test/` } - // Scheme - let scheme = scheme.zeroed(); - if scheme.is_local() && path.as_strand() == "~" { + // Spec + let spec = spec.zeroed(); + if spec.kind.is_local() && path.as_strand() == "~" { return None; // We don't complete a `~`, but `~/` } @@ -81,11 +81,11 @@ impl Trigger { let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_path(), |(_, c)| c).to_owned(); // Parent - let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?; - let abs = if let Some(u) = provider::try_absolute(&url) { u } else { url }; + let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?; + let abs = if let Some(u) = engine::try_absolute(&url) { u } else { url }; let parent = abs.loc().try_strip_suffix(&child).ok()?; - Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child)) + Some((clean_url(UrlCow::try_from((spec, parent)).ok()?), child)) } } @@ -108,10 +108,11 @@ mod tests { #[test] fn test_split() { yazi_shared::init_tests(); + yazi_config::init_tests(); yazi_fs::init(); assert_eq!(Trigger::split_url(""), None); - assert_eq!(Trigger::split_url("sftp://test"), None); + assert_eq!(Trigger::split_url("sftp://vps"), None); compare(" ", "", " "); compare("/", "/", ""); @@ -127,16 +128,18 @@ mod tests { compare("/foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar"); - CWD.set(&"sftp://test".parse::().unwrap(), || {}); - compare("sftp://test/a", "sftp://test/.", "a"); - compare("sftp://test//a", "sftp://test//", "a"); - compare("sftp://test2/a", "sftp://test2/.", "a"); - compare("sftp://test2//a", "sftp://test2//", "a"); + CWD.set(&"sftp://vps".parse::().unwrap(), || {}); + compare("sftp://vps/a", "sftp://vps/.", "a"); + compare("sftp://vps//a", "sftp://vps//", "a"); + compare("test-scope://aws/a", "test-scope://aws/.", "a"); + compare("test-scope://aws//a", "test-scope://aws//", "a"); } #[cfg(windows)] #[test] fn test_split() { + yazi_shared::init_tests(); + yazi_config::init_tests(); yazi_fs::init(); compare("foo", "", "foo"); diff --git a/yazi-actor/src/mgr/bulk_create.rs b/yazi-actor/src/mgr/bulk_create.rs index ba294eb3..ea054392 100644 --- a/yazi-actor/src/mgr/bulk_create.rs +++ b/yazi-actor/src/mgr/bulk_create.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow}; use scopeguard::defer; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRuleArc}; -use yazi_fs::{FilesOp, Splatter, file::File, provider::{Provider, local::Local}}; +use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}, file::File}; use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; @@ -13,7 +13,7 @@ use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_shim::path::CROSS_SEPARATOR; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; -use yazi_vfs::{VfsFile, provider}; +use yazi_vfs::{VfsFile, engine}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -31,7 +31,7 @@ impl Actor for BulkCreate { let cwd = cx.cwd().clone(); tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk-create"); - provider::create_new(&tmp).await?; + engine::create_new(&tmp).await?; defer! { let tmp = tmp.clone(); @@ -76,10 +76,10 @@ impl BulkCreate { }; let result: io::Result<()> = if entry.is_dir { - provider::create_dir_all(&dist).await + engine::create_dir_all(&dist).await } else if let Some(parent) = dist.parent() { - provider::create_dir_all(parent).await.ok(); - provider::create_new(&dist).await.map(|_| ()) + engine::create_dir_all(parent).await.ok(); + engine::create_new(&dist).await.map(|_| ()) } else { Err(io::Error::other("No parent directory")) }; diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 4f42978a..9e2865f5 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -7,7 +7,7 @@ use tokio::io::AsyncWriteExt; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRuleArc}; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, Splatter, file::File, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; +use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::{Demand, Local}}, file::File, max_common_root, path::skip_url}; use yazi_macro::{err, succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; @@ -15,7 +15,7 @@ use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; -use yazi_vfs::{VfsFile, maybe_exists, provider}; +use yazi_vfs::{VfsFile, engine, maybe_exists}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -46,7 +46,7 @@ impl Actor for BulkRename { tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk-rename"); - Gate::default() + Demand::default() .write(true) .create_new(true) .open(&tmp) @@ -127,9 +127,9 @@ impl BulkRename { continue; }; - if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { + if maybe_exists(&new).await && !engine::must_identical(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); - } else if let Err(e) = provider::rename(&old, &new).await { + } else if let Err(e) = engine::rename(&old, &new).await { failed.push((o, n, e.into())); } else if let Ok(f) = File::new(new).await { succeeded.insert(old, f); diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 11da268d..ab852086 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -11,7 +11,7 @@ use yazi_macro::{act, err, input, render, succ}; use yazi_parser::mgr::CdForm; use yazi_proxy::{CmpProxy, MgrProxy}; use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, provider}; +use yazi_vfs::{VfsFile, engine}; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; @@ -73,7 +73,7 @@ impl Cd { match result { InputEvent::Submit(s) => { let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return }; - let Ok(url) = provider::absolute(&url).await else { return }; + let Ok(url) = engine::absolute(&url).await else { return }; let url = clean_url(url); let Ok(file) = File::new(&url).await else { return }; diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 9128720b..0613a212 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -9,7 +9,7 @@ use yazi_macro::{input, ok_or_not_found, succ}; use yazi_parser::mgr::CreateForm; use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, provider}; +use yazi_vfs::{VfsFile, engine}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -62,22 +62,22 @@ impl Create { let _permit = WATCHER.acquire().await.unwrap(); if dir { - provider::create_dir_all(&new).await?; - } else if let Ok(real) = provider::casefold(&new).await + engine::create_dir_all(&new).await?; + } else if let Ok(real) = engine::casefold(&new).await && let Some((parent, urn)) = real.pair() { - ok_or_not_found!(provider::remove_file(&new).await); + ok_or_not_found!(engine::remove_file(&new).await); FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); - provider::create(&new).await?; + engine::create(&new).await?; } else if let Some(parent) = new.parent() { - provider::create_dir_all(parent).await.ok(); - ok_or_not_found!(provider::remove_file(&new).await); - provider::create(&new).await?; + engine::create_dir_all(parent).await.ok(); + ok_or_not_found!(engine::remove_file(&new).await); + engine::create(&new).await?; } else { bail!("Cannot create file at root"); } - if let Ok(real) = provider::casefold(&new).await + if let Ok(real) = engine::casefold(&new).await && let Some((parent, urn)) = real.pair() { let file = File::new(&real).await?; diff --git a/yazi-actor/src/mgr/displace.rs b/yazi-actor/src/mgr/displace.rs index c6339beb..97189b38 100644 --- a/yazi-actor/src/mgr/displace.rs +++ b/yazi-actor/src/mgr/displace.rs @@ -4,7 +4,7 @@ use yazi_macro::succ; use yazi_parser::VoidForm; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::UrlLike}; -use yazi_vfs::provider; +use yazi_vfs::engine; use crate::{Actor, Ctx}; @@ -24,7 +24,7 @@ impl Actor for Displace { let from = cx.cwd().to_owned(); tokio::spawn(async move { MgrProxy::displace_do(tab, DisplaceOpt { - to: provider::canonicalize(&from).await.map_err(Into::into), + to: engine::canonicalize(&from).await.map_err(Into::into), from, }); }); diff --git a/yazi-actor/src/mgr/download.rs b/yazi-actor/src/mgr/download.rs index 61b02d8a..1a6da9ee 100644 --- a/yazi-actor/src/mgr/download.rs +++ b/yazi-actor/src/mgr/download.rs @@ -4,7 +4,7 @@ use anyhow::Result; use futures::{StreamExt, stream::FuturesUnordered}; use hashbrown::HashSet; use yazi_core::mgr::OpenOpt; -use yazi_fs::{FsScheme, file::File, provider::{Provider, local::Local}}; +use yazi_fs::{FsSpec, engine::{Engine, local::Local}, file::File}; use yazi_macro::succ; use yazi_parser::mgr::DownloadForm; use yazi_proxy::MgrProxy; @@ -74,7 +74,7 @@ impl Actor for Download { impl Download { async fn prepare(urls: &[UrlBuf]) { - let roots: HashSet<_> = urls.iter().filter_map(|u| u.scheme().cache()).collect(); + let roots: HashSet<_> = urls.iter().filter_map(|u| u.auth().cache()).collect(); for mut root in roots { root.push("%lock"); Local::regular(&root).create_dir_all().await.ok(); diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index 5d4ae16f..c6692aaa 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -52,11 +52,8 @@ impl Refresh { } } - let futs: Vec<_> = folders - .iter() - .filter(|&f| f.url.is_absolute() && f.url.is_internal()) - .map(|&f| go(f.url.clone(), f.cha)) - .collect(); + let futs: Vec<_> = + folders.iter().filter(|&f| f.url.is_absolute()).map(|&f| go(f.url.clone(), f.cha)).collect(); if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index b654e33c..423640de 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -6,7 +6,7 @@ use yazi_macro::{act, err, input, ok_or_not_found, succ}; use yazi_parser::mgr::RenameForm; use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_shared::{data::Data, id::Id, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, provider}; +use yazi_vfs::{VfsFile, engine}; use yazi_watcher::WATCHER; use yazi_widgets::input::InputEvent; @@ -70,18 +70,18 @@ impl Rename { let Some(_) = new.pair() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); - let overwritten = provider::casefold(&new).await; - provider::rename(&old, &new).await?; + let overwritten = engine::casefold(&new).await; + engine::rename(&old, &new).await?; if let Ok(u) = overwritten && u != new && let Some((parent, urn)) = u.pair() { - ok_or_not_found!(provider::rename(&u, &new).await); + ok_or_not_found!(engine::rename(&u, &new).await); FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); } - let new = provider::casefold(&new).await?; + let new = engine::casefold(&new).await?; let Some((new_p, new_n)) = new.pair() else { return Ok(()) }; let file = File::new(&new).await?; @@ -103,7 +103,7 @@ impl Rename { }; Ok( - provider::must_identical(old, new).await + engine::must_identical(old, new).await || ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await, ) } diff --git a/yazi-actor/src/mgr/stash.rs b/yazi-actor/src/mgr/stash.rs index 89b68a7f..cba5fb97 100644 --- a/yazi-actor/src/mgr/stash.rs +++ b/yazi-actor/src/mgr/stash.rs @@ -13,7 +13,7 @@ impl Actor for Stash { const NAME: &str = "stash"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - if form.target.is_absolute() && form.target.is_internal() { + if form.target.is_absolute() && !form.target.is_search() { cx.tab_mut().backstack.push(form.target.as_url()); } diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index 5afeff56..a917f8ff 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -5,7 +5,7 @@ use image::{DynamicImage, ImageDecoder, ImageError, ImageReader, Limits, codecs: use ratatui_core::layout::Rect; use yazi_config::YAZI; use yazi_emulator::Dimension; -use yazi_fs::provider::{Provider, local::Local}; +use yazi_fs::engine::{Engine, local::Local}; use crate::Icc; diff --git a/yazi-binding/src/chan.rs b/yazi-binding/src/chan.rs index 651f0bc2..91116377 100644 --- a/yazi-binding/src/chan.rs +++ b/yazi-binding/src/chan.rs @@ -4,13 +4,24 @@ use yazi_codegen::FromLuaOwned; use crate::Error; #[derive(FromLuaOwned)] -pub struct MpscTx(pub tokio::sync::mpsc::Sender); +pub struct MpscTx { + tx: tokio::sync::mpsc::Sender, + f: fn(T) -> U, +} pub struct MpscRx(pub tokio::sync::mpsc::Receiver); -impl UserData for MpscTx { +impl MpscTx { + pub fn new(tx: tokio::sync::mpsc::Sender) -> Self { Self { tx, f: |v| v } } +} + +impl MpscTx { + pub fn map(tx: tokio::sync::mpsc::Sender, f: fn(T) -> U) -> Self { Self { tx, f } } +} + +impl UserData for MpscTx { fn add_methods>(methods: &mut M) { methods.add_async_method("send", |lua, me, value: Value| async move { - match me.0.send(T::from_lua(value, &lua)?).await { + match me.tx.send((me.f)(T::from_lua(value, &lua)?)).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua), } diff --git a/yazi-binding/src/error.rs b/yazi-binding/src/error.rs index b13103f6..e20e43f8 100644 --- a/yazi-binding/src/error.rs +++ b/yazi-binding/src/error.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fmt::Display}; +use std::{borrow::Cow, fmt::Display, io}; use mlua::{ExternalError, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; use yazi_codegen::FromLuaOwned; @@ -12,6 +12,17 @@ pub enum Error { Custom(SStr), } +impl From for io::Error { + fn from(value: Error) -> Self { + match value { + Error::Io(e) => e, + Error::Fs(e) => e.into(), + Error::Serde(e) => Self::other(e), + Error::Custom(s) => Self::other(s.into_owned()), + } + } +} + impl Error { pub fn install(lua: &Lua) -> mlua::Result<()> { let custom = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?; diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index 867644e6..a3def6ae 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -2,7 +2,7 @@ use futures::executor::block_on; use hashbrown::HashSet; use yazi_fs::{CWD, path::clean_url}; use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}}; -use yazi_vfs::provider; +use yazi_vfs::engine; #[derive(Debug, Default)] pub struct Boot { @@ -22,7 +22,7 @@ impl Boot { async fn go(entry: &UrlBuf) -> (UrlBuf, StrandBuf) { let mut entry = clean_url(entry); - if let Ok(u) = provider::absolute(&entry).await + if let Ok(u) = engine::absolute(&entry).await && u.is_owned() { entry = u.into_owned(); @@ -32,7 +32,7 @@ impl Boot { return (entry, Default::default()); }; - if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) { + if engine::metadata(&entry).await.is_ok_and(|m| m.is_file()) { (parent.into(), child.into()) } else { (entry, Default::default()) diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index 9f548c1f..113484ca 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, provider::{Provider, local::Local}}; +use yazi_fs::{engine::{Engine, local::Local}, ok_or_not_found}; use yazi_macro::outln; use super::Dependency; diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index ffbe5f5e..c1d428cc 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::provider::{Provider, local::Local}; +use yazi_fs::engine::{Engine, local::Local}; use yazi_macro::outln; use super::Dependency; diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index 39a4e62b..378dbc28 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::provider::local::Local; +use yazi_fs::engine::local::Local; use yazi_macro::ok_or_not_found; use super::Dependency; diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index 40deb14b..159f00d3 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, provider::{Provider, local::Local}}; +use yazi_fs::{Xdg, engine::{Engine, local::Local}}; use yazi_macro::{ok_or_not_found, outln}; use super::Dependency; diff --git a/yazi-cli/src/shared/shared.rs b/yazi-cli/src/shared/shared.rs index 387b5ccb..d276bc33 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::provider::{FileBuilder, Provider, local::{Gate, Local}}; +use yazi_fs::engine::{Engine, FileBuilder, local::{Demand, Local}}; use yazi_macro::ok_or_not_found; #[inline] @@ -21,7 +21,7 @@ pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { let b = Local::regular(from).read().await?; ok_or_not_found!(remove_sealed(to).await); - let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?; + let mut file = Demand::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-codegen/src/lib.rs b/yazi-codegen/src/lib.rs index d3b247b3..0c10562c 100644 --- a/yazi-codegen/src/lib.rs +++ b/yazi-codegen/src/lib.rs @@ -54,7 +54,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream { impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause { fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result { use serde::de::{Error, IgnoredAny, MapAccess, Visitor}; - use yazi_shared::KebabCasedString; + use yazi_shared::KebabCasedKey; use yazi_shim::{serde::single_map_entry, toml::{DeserializeOverHook, DeserializeOverSeed, DeserializeOverWith}}; struct V #impl_generics (#ident #ty_generics) #where_clause; @@ -67,7 +67,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream { } fn visit_map<__M: MapAccess<'__de>>(mut self, mut map: __M) -> Result { - while let Some(key) = map.next_key::()? { + while let Some(key) = map.next_key::()? { match key.as_ref() { #(#normal_arms,)* #flatten_arm diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 7468d153..6a42d274 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -1,6 +1,6 @@ -yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which); +yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme vfs which); -yazi_macro::mod_flat!(icon inject layout mixing pattern platform preset priority selectable selector yazi); +yazi_macro::mod_flat!(icon inject layout mixing pattern platform preset priority selectable selector tests yazi); use std::io::{Read, Write}; @@ -11,6 +11,7 @@ use yazi_tty::{TTY, sequence::SetSgr}; pub static YAZI: RoCell = RoCell::new(); pub static KEYMAP: RoCell = RoCell::new(); pub static THEME: RoCell = RoCell::new(); +pub static VFS: RoCell = RoCell::new(); pub static LAYOUT: SyncCell = SyncCell::new(Layout::default()); pub fn init() -> anyhow::Result<()> { @@ -24,14 +25,17 @@ pub fn init() -> anyhow::Result<()> { fn try_init(merge: bool) -> anyhow::Result<()> { let mut yazi = Preset::yazi()?; let mut keymap = Preset::keymap()?; + let mut vfs = Preset::vfs()?; if merge { yazi = yazi.deserialize_over(&yazi::Yazi::read()?)?; keymap = keymap.deserialize_over(&keymap::Keymap::read()?)?; + vfs = vfs.deserialize_over(&vfs::Vfs::read()?)?; } YAZI.init(yazi); KEYMAP.init(keymap); + VFS.init(vfs); Ok(()) } diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 37fd6dc3..a632b0a8 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, str::FromStr}; use anyhow::{Result, bail}; use globset::{Candidate, GlobBuilder}; use serde::Deserialize; -use yazi_shared::{scheme::SchemeKind, url::AsUrl}; +use yazi_shared::{auth::Auth, url::AsUrl}; use crate::Mixable; @@ -35,7 +35,7 @@ impl Pattern { if is_dir != self.is_dir { return false; - } else if !self.scheme.matches(url.kind()) { + } else if !self.scheme.matches(url.auth()) { return false; } else if self.is_star { return true; @@ -112,59 +112,43 @@ impl Mixable for Pattern { } // --- Scheme -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] enum PatternScheme { Any, Local, Remote, Virtual, - Regular, - Search, - Archive, - Sftp, + Custom(String), } impl PatternScheme { fn parse(s: &str) -> Result<(Self, usize)> { - let Some((protocol, _)) = s.split_once("://") else { + let Some((s, _)) = s.split_once("://") else { return Ok((Self::Any, 0)); }; - let scheme = match protocol { + let scheme = match s { "*" => Self::Any, "local" => Self::Local, "remote" => Self::Remote, "virtual" => Self::Virtual, - "regular" => Self::Regular, - "search" => Self::Search, - "archive" => Self::Archive, - "sftp" => Self::Sftp, - - "" => bail!("Invalid URL pattern: protocol is empty"), - _ => bail!("Unknown protocol in URL pattern: {protocol}"), + "" => bail!("Invalid URL pattern: scheme is empty"), + other => Self::Custom(other.to_owned()), }; - Ok((scheme, protocol.len() + 3)) + Ok((scheme, s.len() + 3)) } #[inline] - fn matches(self, kind: SchemeKind) -> bool { - use SchemeKind as K; - - match (self, kind) { - (Self::Any, _) => true, - (Self::Local, s) => s.is_local(), - (Self::Remote, s) => s.is_remote(), - (Self::Virtual, s) => s.is_virtual(), - - (Self::Regular, K::Regular) => true, - (Self::Search, K::Search) => true, - (Self::Archive, K::Archive) => true, - (Self::Sftp, K::Sftp) => true, - - _ => false, + fn matches(&self, auth: &Auth) -> bool { + match self { + Self::Any => true, + Self::Local => auth.kind.is_local(), + Self::Remote => auth.kind.is_remote(), + Self::Virtual => auth.kind.is_virtual(), + Self::Custom(name) => auth.scheme == name, } } } @@ -183,6 +167,8 @@ mod tests { #[cfg(unix)] #[test] fn test_unix() { + yazi_shared::init_tests(); + // Wildcard assert!(matches("*", "/foo")); assert!(matches("*", "/foo/bar")); @@ -217,6 +203,8 @@ mod tests { #[cfg(windows)] #[test] fn test_windows() { + yazi_shared::init_tests(); + // Wildcard assert!(matches("*", r#"C:\foo"#)); assert!(matches("*", r#"C:\foo\bar"#)); diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index 8abebacf..bb4d1659 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use yazi_binding::position::{Offset, Origin, Position}; use yazi_codegen::{DeserializeOver, DeserializeOver2}; -use yazi_shared::{scheme::Encode as EncodeScheme, url::Url}; +use yazi_shared::{spec::Encode as EncodeSpec, url::Url}; use yazi_widgets::input::InputOpt; #[derive(Deserialize, DeserializeOver, DeserializeOver2)] @@ -49,7 +49,7 @@ impl Input { InputOpt { name: "cd".to_owned(), title: self.cd_title.clone(), - value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, + value: if cwd.kind().is_local() { String::new() } else { EncodeSpec(cwd).to_string() }, history: "shared".to_owned(), position: Position::new(self.cd_origin, self.cd_offset), completion: true, diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 06cc992f..f4d7bdb0 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -1,4 +1,4 @@ -use crate::{Yazi, keymap::Keymap, theme::Theme}; +use crate::{Yazi, keymap::Keymap, theme::Theme, vfs::Vfs}; pub(crate) struct Preset; @@ -11,6 +11,10 @@ impl Preset { toml::from_str(&yazi_macro::config_preset!("keymap")) } + pub(super) fn vfs() -> Result { + toml::from_str(&yazi_macro::config_preset!("vfs")) + } + pub(super) fn theme(light: bool) -> Result { toml::from_str(&if light { yazi_macro::theme_preset!("light") diff --git a/yazi-config/src/tests.rs b/yazi-config/src/tests.rs new file mode 100644 index 00000000..4f955f41 --- /dev/null +++ b/yazi-config/src/tests.rs @@ -0,0 +1,9 @@ +use std::sync::OnceLock; + +use crate::{Preset, VFS}; + +pub fn init_tests() { + static INIT: OnceLock<()> = OnceLock::new(); + + INIT.get_or_init(|| VFS.init(Preset::vfs().unwrap())); +} diff --git a/yazi-config/src/theme/custom.rs b/yazi-config/src/theme/custom.rs index c6e3fd29..da7c734f 100644 --- a/yazi-config/src/theme/custom.rs +++ b/yazi-config/src/theme/custom.rs @@ -4,7 +4,7 @@ use arc_swap::ArcSwap; use hashbrown::{HashMap, hash_map}; use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}}; use yazi_codegen::{DeserializeOver, Overlay}; -use yazi_shared::{KebabCasedString, SnakeCasedString}; +use yazi_shared::{KebabCasedKey, SnakeCasedString}; use yazi_shim::{arc_swap::IntoPointee, toml::DeserializeOverWith}; use crate::theme::CustomSection; @@ -39,7 +39,7 @@ impl<'de> Deserialize<'de> for Custom { fn visit_map>(self, mut map: M) -> Result { let mut sections = HashMap::with_capacity(map.size_hint().unwrap_or(0)); - while let Some(key) = map.next_key::()? { + while let Some(key) = map.next_key::()? { let section = map.next_value::()?; if !section.load().is_empty() { sections.insert(key.into_snake_cased(), section); @@ -64,7 +64,7 @@ impl DeserializeOverWith for Custom { fn visit_map>(self, mut map: M) -> Result { let mut sections = self.0.unwrap_unchecked(); - while let Some(key) = map.next_key::()? { + while let Some(key) = map.next_key::()? { let (key, new) = (key.into_snake_cased(), map.next_value::()?); match sections.entry(key) { hash_map::Entry::Occupied(mut oe) => { diff --git a/yazi-config/src/vfs/lua.rs b/yazi-config/src/vfs/lua.rs new file mode 100644 index 00000000..f94e2d05 --- /dev/null +++ b/yazi-config/src/vfs/lua.rs @@ -0,0 +1,17 @@ +use std::{ops::Deref, sync::Arc}; + +use serde::Deserialize; +use yazi_shared::{auth::Auth, event::Cmd}; + +#[derive(Deserialize)] +pub struct ServiceLua { + #[serde(flatten)] + pub auth: Arc, + pub run: Cmd, +} + +impl Deref for ServiceLua { + type Target = Auth; + + fn deref(&self) -> &Self::Target { &self.auth } +} diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs new file mode 100644 index 00000000..4481f651 --- /dev/null +++ b/yazi-config/src/vfs/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(lua service services sftp vfs); diff --git a/yazi-config/src/vfs/service.rs b/yazi-config/src/vfs/service.rs new file mode 100644 index 00000000..9c5604fa --- /dev/null +++ b/yazi-config/src/vfs/service.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use serde::Deserialize; +use yazi_shared::auth::{Auth, AuthKind}; + +use super::{ServiceLua, ServiceSftp}; + +#[derive(Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case")] +pub enum Service { + Sftp(ServiceSftp), + Mount(ServiceLua), + Scope(ServiceLua), +} + +impl TryFrom<&'static Service> for &'static ServiceSftp { + type Error = &'static str; + + fn try_from(value: &'static Service) -> Result { + match value { + Service::Sftp(p) => Ok(p), + Service::Mount(_) | Service::Scope(_) => { + Err("expected an SFTP service, got a custom VFS service") + } + } + } +} + +impl TryFrom<&'static Service> for &'static ServiceLua { + type Error = &'static str; + + fn try_from(value: &'static Service) -> Result { + match value { + Service::Sftp(_) => Err("expected a custom VFS service, got an SFTP service"), + Service::Mount(lua) | Service::Scope(lua) => Ok(lua), + } + } +} + +impl Service { + pub fn kind(&self) -> AuthKind { + match self { + Self::Sftp(_) => AuthKind::Sftp, + Self::Mount(_) => AuthKind::Mount, + Self::Scope(_) => AuthKind::Scope, + } + } + + pub fn auth(&self) -> &Arc { + match self { + Self::Sftp(sftp) => &sftp.auth, + Self::Mount(lua) => &lua.auth, + Self::Scope(lua) => &lua.auth, + } + } + + pub fn auth_mut(&mut self) -> &mut Arc { + match self { + Self::Sftp(sftp) => &mut sftp.auth, + Self::Mount(lua) => &mut lua.auth, + Self::Scope(lua) => &mut lua.auth, + } + } +} diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs new file mode 100644 index 00000000..46c1d1c8 --- /dev/null +++ b/yazi-config/src/vfs/services.rs @@ -0,0 +1,47 @@ +use std::{ops::{Deref, DerefMut}, sync::Arc}; + +use hashbrown::HashMap; +use serde::{Deserialize, Deserializer}; +use yazi_shared::KebabCasedKey; +use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith}; + +use super::Service; + +pub struct Services(HashMap); + +impl Deref for Services { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Services { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl<'de> Deserialize<'de> for Services { + fn deserialize>(deserializer: D) -> Result { + Self(HashMap::deserialize(deserializer)?) + .deserialize_over_hook() + .map_err(serde::de::Error::custom) + } +} + +impl DeserializeOverWith for Services { + fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result { + Ok(Self(self.0.deserialize_over_with(de)?)) + } +} + +impl DeserializeOverHook for Services { + fn deserialize_over_hook(mut self) -> Result { + for (domain, service) in &mut self.0 { + let kind = service.kind(); + let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); + + auth.kind = kind; + auth.domain = domain.to_string().into(); + } + Ok(self) + } +} diff --git a/yazi-vfs/src/config/sftp.rs b/yazi-config/src/vfs/sftp.rs similarity index 80% rename from yazi-vfs/src/config/sftp.rs rename to yazi-config/src/vfs/sftp.rs index 81d67e88..6f3a5615 100644 --- a/yazi-vfs/src/config/sftp.rs +++ b/yazi-config/src/vfs/sftp.rs @@ -1,10 +1,13 @@ -use std::{env, path::PathBuf}; +use std::{env, ops::Deref, path::PathBuf, sync::Arc}; use serde::{Deserialize, Deserializer, Serialize, de}; use yazi_fs::path::sanitize_path; +use yazi_shared::auth::{Auth, AuthKind, Scheme}; #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ServiceSftp { + #[serde(skip, default = "default_auth")] + pub auth: Arc, pub host: String, pub user: String, pub port: u16, @@ -20,6 +23,14 @@ pub struct ServiceSftp { pub identity_agent: PathBuf, } +impl Deref for ServiceSftp { + type Target = Auth; + + fn deref(&self) -> &Self::Target { &self.auth } +} + +fn default_auth() -> Arc { Auth::new(AuthKind::Sftp, Scheme::Sftp, "") } + fn deserialize_path<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs new file mode 100644 index 00000000..7d936147 --- /dev/null +++ b/yazi-config/src/vfs/vfs.rs @@ -0,0 +1,53 @@ +use std::io; + +use anyhow::{Context, Result}; +use serde::Deserialize; +use yazi_codegen::{DeserializeOver, DeserializeOver1}; +use yazi_fs::{Xdg, ok_or_not_found}; +use yazi_shared::auth::AuthInventory; + +use super::{Service, Services}; +use crate::VFS; + +#[derive(Deserialize, DeserializeOver, DeserializeOver1)] +pub struct Vfs { + pub services: Services, +} + +impl Vfs { + pub fn service

(domain: &str) -> io::Result

+ where + P: TryFrom<&'static Service, Error = &'static str>, + { + let Some((key, value)) = VFS.services.get_key_value(domain) else { + return Err(io::Error::other(format!("No such VFS service: {domain}"))); + }; + + match value.try_into() { + Ok(p) => Ok(p), + Err(e) => Err(io::Error::other(format!("VFS service `{key}` has wrong kind: {e}"))), + } + } + + pub(crate) fn read() -> Result { + let p = Xdg::config_dir().join("vfs.toml"); + ok_or_not_found(std::fs::read_to_string(&p)) + .with_context(|| format!("Failed to read config {p:?}")) + } +} + +// --- Inject +inventory::submit! { + AuthInventory { + get: |scheme, domain| { + VFS.services.get(domain).and_then(|service| { + let auth = service.auth(); + if auth.scheme == scheme { + Some(auth.clone()) + } else { + None + } + }) + }, + } +} diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index f726bd56..fecb4de4 100644 --- a/yazi-core/src/mgr/snap.rs +++ b/yazi-core/src/mgr/snap.rs @@ -28,7 +28,7 @@ impl Splatable for MgrSnap { .checked_sub(1) .and_then(|tab| self.tabs.get(tab)) .map_or_else(|| &[][..], |s| &s.selected) - .into_iter() + .iter() .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) .map(AsUrl::as_url) diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index a05ecff5..da4a2f0b 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -7,7 +7,7 @@ use yazi_config::{LAYOUT, YAZI}; use yazi_fs::{Entries, FilesOp, cha::Cha, file::File}; use yazi_macro::render; use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}}; -use yazi_shared::{pool::Symbol, url::{UrlBuf, UrlLike}}; +use yazi_shared::{pool::Symbol, url::UrlBuf}; use yazi_vfs::{VfsEntries, VfsFilesOp}; use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock}; @@ -48,9 +48,7 @@ impl Preview { } pub fn go_folder(&mut self, file: File, dir: Option, mime: Symbol, force: bool) { - if !file.url.is_internal() { - return self.go(file, mime, force); - } else if self.folder_lock.as_ref() == Some(&file.url) { + if self.folder_lock.as_ref() == Some(&file.url) { return self.go(file, mime, force); } diff --git a/yazi-dds/src/state.rs b/yazi-dds/src/state.rs index c740921a..1488a16e 100644 --- a/yazi-dds/src/state.rs +++ b/yazi-dds/src/state.rs @@ -4,7 +4,7 @@ use anyhow::Result; use hashbrown::HashMap; use parking_lot::RwLock; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; -use yazi_fs::{Xdg, provider::{FileBuilder, Provider, local::{Gate, Local}}}; +use yazi_fs::{Xdg, engine::{Engine, FileBuilder, local::{Demand, Local}}}; use yazi_shared::timestamp_us; use yazi_shim::cell::RoCell; @@ -63,7 +63,12 @@ impl State { Local::regular(&state_dir).create_dir_all().await?; let mut buf = BufWriter::new( - Gate::default().write(true).create(true).truncate(true).open(state_dir.join(".dds")).await?, + Demand::default() + .write(true) + .create(true) + .truncate(true) + .open(state_dir.join(".dds")) + .await?, ); let mut state = inner.into_iter().collect::>(); diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index 5aba44bd..51eac132 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -1,7 +1,7 @@ use std::{io, path::PathBuf}; use tokio::{io::{AsyncBufReadExt, BufReader, Lines, ReadHalf, WriteHalf}, sync::OnceCell}; -use yazi_fs::{Xdg, create_owned_dir, provider::{Provider, local::Local}}; +use yazi_fs::{Xdg, create_owned_dir, engine::{Engine, local::Local}}; pub struct Stream; diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index f6710142..9db4c7ab 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -16,12 +16,14 @@ async fn main() -> anyhow::Result<()> { yazi_fs::init(); - yazi_vfs::init(); - yazi_tty::init(); yazi_config::init()?; + yazi_vfs::init(); + + yazi_runner::init(yazi_plugin::slim_lua); + yazi_boot::init(); yazi_term::init()?; @@ -34,8 +36,6 @@ async fn main() -> anyhow::Result<()> { yazi_watcher::init(); - yazi_runner::init(yazi_plugin::slim_lua); - yazi_plugin::init()?; yazi_dds::serve(); diff --git a/yazi-fs/src/cha/mode.rs b/yazi-fs/src/cha/mode.rs index fd2d2db7..0dd5a0b0 100644 --- a/yazi-fs/src/cha/mode.rs +++ b/yazi-fs/src/cha/mode.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use anyhow::{anyhow, bail}; use bitflags::bitflags; +use mlua::{IntoLua, Lua, Value}; use serde::{Deserialize, Serialize}; use crate::cha::ChaType; @@ -158,3 +159,7 @@ impl ChaMode { #[inline] pub const fn is_sticky(self) -> bool { self.contains(Self::S_STICKY) } } + +impl IntoLua for ChaMode { + fn into_lua(self, lua: &Lua) -> mlua::Result { self.bits().into_lua(lua) } +} diff --git a/yazi-fs/src/provider/attrs.rs b/yazi-fs/src/engine/attrs.rs similarity index 75% rename from yazi-fs/src/provider/attrs.rs rename to yazi-fs/src/engine/attrs.rs index 5e25f9b0..47fc9a19 100644 --- a/yazi-fs/src/provider/attrs.rs +++ b/yazi-fs/src/engine/attrs.rs @@ -1,5 +1,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use mlua::{IntoLua, Lua, Value}; + use crate::cha::{Cha, ChaMode}; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -63,6 +65,8 @@ impl TryFrom for std::fs::Permissions { } impl Attrs { + pub fn mode(mode: ChaMode) -> Self { Self { mode: Some(mode), ..Default::default() } } + pub fn has_times(self) -> bool { self.atime.is_some() || self.btime.is_some() || self.mtime.is_some() } @@ -73,3 +77,16 @@ impl Attrs { pub fn mtime_dur(self) -> Option { self.mtime?.duration_since(UNIX_EPOCH).ok() } } + +impl IntoLua for Attrs { + fn into_lua(self, lua: &Lua) -> mlua::Result { + lua + .create_table_from([ + ("mode", self.mode.map(|m| m.bits()).into_lua(lua)?), + ("atime", self.atime_dur().map(|d| d.as_secs_f64()).into_lua(lua)?), + ("btime", self.btime_dur().map(|d| d.as_secs_f64()).into_lua(lua)?), + ("mtime", self.mtime_dur().map(|d| d.as_secs_f64()).into_lua(lua)?), + ])? + .into_lua(lua) + } +} diff --git a/yazi-fs/src/engine/capabilities.rs b/yazi-fs/src/engine/capabilities.rs new file mode 100644 index 00000000..e6c2d2ee --- /dev/null +++ b/yazi-fs/src/engine/capabilities.rs @@ -0,0 +1,22 @@ +use mlua::{FromLua, Lua, Table, Value}; + +#[derive(Clone, Copy, Debug, Default)] +pub struct Capabilities { + pub symlink: bool, + pub hard_link: bool, + pub trash: bool, + pub copy_progressive: bool, +} + +impl FromLua for Capabilities { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let t = Table::from_lua(value, lua)?; + + Ok(Self { + symlink: t.raw_get("symlink")?, + hard_link: t.raw_get("hard_link")?, + trash: t.raw_get("trash")?, + copy_progressive: t.raw_get("copy_progressive")?, + }) + } +} diff --git a/yazi-fs/src/engine/demand.rs b/yazi-fs/src/engine/demand.rs new file mode 100644 index 00000000..7aa85be0 --- /dev/null +++ b/yazi-fs/src/engine/demand.rs @@ -0,0 +1,55 @@ +use mlua::{IntoLua, Lua, Value}; + +use super::{Attrs, FileBuilder}; + +#[derive(Clone, Copy, Default)] +pub struct Demand { + pub attrs: Attrs, + pub append: bool, + pub create: bool, + pub create_new: bool, + pub read: bool, + pub truncate: bool, + pub write: bool, +} + +impl Demand { + pub fn build(self) -> T { + let mut demand = T::default(); + demand.attrs(self.attrs); + + if self.append { + demand.append(true); + } + if self.create { + demand.create(true); + } + if self.create_new { + demand.create_new(true); + } + if self.read { + demand.read(true); + } + if self.truncate { + demand.truncate(true); + } + if self.write { + demand.write(true); + } + demand + } +} + +impl IntoLua for Demand { + fn into_lua(self, lua: &Lua) -> mlua::Result { + let t = lua.create_table_from([ + ("append", self.append), + ("create", self.create), + ("create_new", self.create_new), + ("read", self.read), + ("truncate", self.truncate), + ("write", self.write), + ])?; + t.into_lua(lua) + } +} diff --git a/yazi-fs/src/provider/local/absolute.rs b/yazi-fs/src/engine/local/absolute.rs similarity index 86% rename from yazi-fs/src/provider/local/absolute.rs rename to yazi-fs/src/engine/local/absolute.rs index d22cceda..091c2d84 100644 --- a/yazi-fs/src/provider/local/absolute.rs +++ b/yazi-fs/src/engine/local/absolute.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use yazi_shared::{loc::LocBuf, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::CWD; @@ -50,7 +50,7 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option> { Some(match url.as_url() { Url::Regular(_) => UrlBuf::Regular(loc).into(), - Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(), - Url::Archive { .. } | Url::Sftp { .. } => None?, + Url::Search { auth, .. } => UrlBuf::Search { loc, auth: auth.clone() }.into(), + Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?, }) } diff --git a/yazi-fs/src/provider/local/calculator.rs b/yazi-fs/src/engine/local/calculator.rs similarity index 100% rename from yazi-fs/src/provider/local/calculator.rs rename to yazi-fs/src/engine/local/calculator.rs diff --git a/yazi-fs/src/provider/local/casefold.rs b/yazi-fs/src/engine/local/casefold.rs similarity index 100% rename from yazi-fs/src/provider/local/casefold.rs rename to yazi-fs/src/engine/local/casefold.rs diff --git a/yazi-fs/src/provider/local/copier.rs b/yazi-fs/src/engine/local/copier.rs similarity index 97% rename from yazi-fs/src/provider/local/copier.rs rename to yazi-fs/src/engine/local/copier.rs index 528227c0..256138be 100644 --- a/yazi-fs/src/provider/local/copier.rs +++ b/yazi-fs/src/engine/local/copier.rs @@ -2,7 +2,7 @@ use std::{io, path::PathBuf}; use tokio::{select, sync::{mpsc, oneshot}}; -use crate::provider::Attrs; +use crate::engine::Attrs; pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::Result { #[cfg(any(target_os = "linux", target_os = "android"))] @@ -48,7 +48,7 @@ pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::R } } -pub(super) fn copy_with_progress_impl( +pub(super) fn copy_progressive_impl( from: PathBuf, to: PathBuf, attrs: Attrs, diff --git a/yazi-fs/src/provider/local/gate.rs b/yazi-fs/src/engine/local/demand.rs similarity index 90% rename from yazi-fs/src/provider/local/gate.rs rename to yazi-fs/src/engine/local/demand.rs index 2136b2ec..f0cb61d3 100644 --- a/yazi-fs/src/provider/local/gate.rs +++ b/yazi-fs/src/engine/local/demand.rs @@ -2,12 +2,12 @@ use std::io; use yazi_shared::url::AsUrl; -use crate::provider::{Attrs, FileBuilder}; +use crate::engine::{Attrs, FileBuilder}; #[derive(Default)] -pub struct Gate(tokio::fs::OpenOptions); +pub struct Demand(tokio::fs::OpenOptions); -impl FileBuilder for Gate { +impl FileBuilder for Demand { type File = tokio::fs::File; fn append(&mut self, append: bool) -> &mut Self { diff --git a/yazi-fs/src/provider/local/dir_entry.rs b/yazi-fs/src/engine/local/dir_entry.rs similarity index 95% rename from yazi-fs/src/provider/local/dir_entry.rs rename to yazi-fs/src/engine/local/dir_entry.rs index aa126a85..fb494640 100644 --- a/yazi-fs/src/provider/local/dir_entry.rs +++ b/yazi-fs/src/engine/local/dir_entry.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; -use crate::{cha::{Cha, ChaType}, provider::FileHolder}; +use crate::{cha::{Cha, ChaType}, engine::FileHolder}; pub enum DirEntry { Regular(tokio::fs::DirEntry), diff --git a/yazi-fs/src/provider/local/identical.rs b/yazi-fs/src/engine/local/identical.rs similarity index 100% rename from yazi-fs/src/provider/local/identical.rs rename to yazi-fs/src/engine/local/identical.rs diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/engine/local/local.rs similarity index 74% rename from yazi-fs/src/provider/local/local.rs rename to yazi-fs/src/engine/local/local.rs index 6ec0557d..32e476d3 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/engine/local/local.rs @@ -1,9 +1,9 @@ -use std::{io, path::Path, sync::Arc}; +use std::{fs::FileTimes, io, path::Path, sync::Arc}; use tokio::sync::mpsc; -use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{auth::AuthKind, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; -use crate::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider}}; +use crate::{cha::{Cha, ChaMode}, engine::{Attrs, Capabilities, Engine}}; #[derive(Clone)] pub struct Local<'a> { @@ -11,9 +11,9 @@ pub struct Local<'a> { path: &'a Path, } -impl<'a> Provider for Local<'a> { +impl<'a> Engine for Local<'a> { + type Demand = super::Demand; type File = tokio::fs::File; - type Gate = super::Gate; type Me<'b> = Local<'b>; type ReadDir = super::ReadDir; type UrlCow = UrlCow<'a>; @@ -29,7 +29,14 @@ impl<'a> Provider for Local<'a> { } #[inline] - fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } } + async fn capabilities(&self) -> io::Result { + Ok(Capabilities { + symlink: true, + hard_link: true, + trash: true, + copy_progressive: true, + }) + } async fn casefold(&self) -> io::Result { super::casefold(self.path).await.map(Into::into) @@ -45,14 +52,14 @@ impl<'a> Provider for Local<'a> { super::copy_impl(from, to, attrs).await } - fn copy_with_progress(&self, to: P, attrs: A) -> io::Result>> + fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where P: AsPath, A: Into, { let to = to.as_path().to_os_owned()?; let from = self.path.to_owned(); - Ok(super::copy_with_progress_impl(from, to, attrs.into())) + Ok(super::copy_progressive_impl(from, to, attrs.into())) } #[inline] @@ -80,7 +87,7 @@ impl<'a> Provider for Local<'a> { async fn new<'b>(url: Url<'b>) -> io::Result> { match url { Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }), - Url::Archive { .. } | Url::Sftp { .. } => { + Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => { Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) } } @@ -89,12 +96,12 @@ impl<'a> Provider for Local<'a> { #[inline] async fn read_dir(self) -> io::Result { Ok(match self.url.kind() { - SchemeKind::Regular => Self::ReadDir::Regular(tokio::fs::read_dir(self.path).await?), - SchemeKind::Search => Self::ReadDir::Others { + AuthKind::Regular => Self::ReadDir::Regular(tokio::fs::read_dir(self.path).await?), + AuthKind::Search => Self::ReadDir::Others { reader: tokio::fs::read_dir(self.path).await?, dir: Arc::new(self.url.to_owned()), }, - SchemeKind::Archive | SchemeKind::Sftp => Err(io::Error::new( + AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Not a local URL: {:?}", self.url), ))?, @@ -125,24 +132,19 @@ impl<'a> Provider for Local<'a> { tokio::fs::rename(self.path, to).await } - async fn set_mode(&self, mode: ChaMode) -> io::Result<()> { - #[cfg(unix)] - { - return tokio::fs::set_permissions(self.path, mode.into()).await; + async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { + let (mode, times) = (attrs.mode, attrs.try_into()); + if mode.is_none() && times.is_err() { + return Ok(()); } - #[cfg(windows)] - { - use std::os::windows::ffi::OsStrExt; - let path: Vec = self.path.as_os_str().encode_wide().chain(Some(0)).collect(); - let perm = if mode.contains(ChaMode::U_WRITE) { libc::S_IWRITE } else { libc::S_IREAD }; - - return tokio::task::spawn_blocking(move || { - let result = unsafe { libc::wchmod(path.as_ptr(), perm) }; - if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) } - }) - .await?; - } + let path = self.path.to_owned(); + tokio::task::spawn_blocking(move || { + let a = mode.map_or(Ok(()), |mode| Self::set_mode(&path, mode)); + let b = times.map_or(Ok(()), |times| Self::set_times(&path, times)); + a.and(b) + }) + .await? } #[inline] @@ -256,4 +258,26 @@ impl<'a> Local<'a> { { Self { url: Url::regular(path), path: path.as_ref() } } + + fn set_mode(path: &Path, mode: ChaMode) -> io::Result<()> { + #[cfg(unix)] + { + std::fs::set_permissions(path, mode.into()) + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let path: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + let perm = if mode.contains(ChaMode::U_WRITE) { libc::S_IWRITE } else { libc::S_IREAD }; + + let result = unsafe { libc::wchmod(path.as_ptr(), perm) }; + if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) } + } + } + + fn set_times(path: &Path, times: FileTimes) -> io::Result<()> { + std::fs::File::open(path)?.set_times(times) + } } diff --git a/yazi-fs/src/provider/local/mod.rs b/yazi-fs/src/engine/local/mod.rs similarity index 62% rename from yazi-fs/src/provider/local/mod.rs rename to yazi-fs/src/engine/local/mod.rs index a7d3e2e3..d837d4c3 100644 --- a/yazi-fs/src/provider/local/mod.rs +++ b/yazi-fs/src/engine/local/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry gate identical local read_dir); +yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry demand identical local read_dir); diff --git a/yazi-fs/src/provider/local/read_dir.rs b/yazi-fs/src/engine/local/read_dir.rs similarity index 94% rename from yazi-fs/src/provider/local/read_dir.rs rename to yazi-fs/src/engine/local/read_dir.rs index 78ee07c9..4a3332e8 100644 --- a/yazi-fs/src/provider/local/read_dir.rs +++ b/yazi-fs/src/engine/local/read_dir.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use yazi_shared::url::UrlBuf; -use crate::provider::DirReader; +use crate::engine::DirReader; pub enum ReadDir { Regular(tokio::fs::ReadDir), diff --git a/yazi-fs/src/engine/mod.rs b/yazi-fs/src/engine/mod.rs new file mode 100644 index 00000000..15c19b54 --- /dev/null +++ b/yazi-fs/src/engine/mod.rs @@ -0,0 +1,3 @@ +yazi_macro::mod_pub!(local); + +yazi_macro::mod_flat!(attrs capabilities demand traits); diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/engine/traits.rs similarity index 86% rename from yazi-fs/src/provider/traits.rs rename to yazi-fs/src/engine/traits.rs index 0c9bb0d6..c26ee590 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/engine/traits.rs @@ -4,20 +4,20 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; -use crate::{cha::{Cha, ChaMode, ChaType}, provider::{Attrs, Capabilities}}; +use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}}; -pub trait Provider: Sized { +pub trait Engine: Sized { type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; - type Gate: FileBuilder; + type Demand: FileBuilder; type ReadDir: DirReader + 'static; type UrlCow; - type Me<'a>: Provider; + type Me<'a>: Engine; fn absolute(&self) -> impl Future>; fn canonicalize(&self) -> impl Future>; - fn capabilities(&self) -> Capabilities; + fn capabilities(&self) -> impl Future>; fn casefold(&self) -> impl Future>; @@ -25,17 +25,13 @@ pub trait Provider: Sized { where P: AsPath; - fn copy_with_progress( - &self, - to: P, - attrs: A, - ) -> io::Result>> + fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where P: AsPath, A: Into; fn create(&self) -> impl Future> { - async move { self.gate().write(true).create(true).truncate(true).open(self.url()).await } + async move { self.demand().write(true).create(true).truncate(true).open(self.url()).await } } fn create_dir(&self) -> impl Future>; @@ -77,10 +73,10 @@ pub trait Provider: Sized { } fn create_new(&self) -> impl Future> { - async move { self.gate().write(true).create_new(true).open(self.url()).await } + async move { self.demand().write(true).create_new(true).open(self.url()).await } } - fn gate(&self) -> Self::Gate { Self::Gate::default() } + fn demand(&self) -> Self::Demand { Self::Demand::default() } fn hard_link

(&self, to: P) -> impl Future> where @@ -91,7 +87,7 @@ pub trait Provider: Sized { fn new<'a>(url: Url<'a>) -> impl Future>>; fn open(&self) -> impl Future> { - async move { self.gate().read(true).open(self.url()).await } + async move { self.demand().read(true).open(self.url()).await } } fn read_dir(self) -> impl Future>; @@ -103,7 +99,7 @@ pub trait Provider: Sized { fn remove_dir_all(&self) -> impl Future> { async fn remove_dir_all_impl

(url: Url<'_>) -> io::Result<()> where - P: Provider, + P: Engine, { let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(())); while let Some(child) = it.next().await? { @@ -138,13 +134,13 @@ pub trait Provider: Sized { let mut result = Ok(()); while let Some((dir, visited)) = stack.pop() { - let Ok(provider) = Self::new(dir.as_url()).await else { + let Ok(engine) = Self::new(dir.as_url()).await else { continue; }; if visited { - result = provider.remove_dir().await; - } else if let Ok(mut it) = provider.read_dir().await { + result = engine.remove_dir().await; + } else if let Ok(mut it) = engine.read_dir().await { stack.push((dir, true)); while let Ok(Some(ent)) = it.next().await { if ent.file_type().await.is_ok_and(|t| t.is_dir()) { @@ -163,7 +159,7 @@ pub trait Provider: Sized { where P: AsPath; - fn set_mode(&self, mode: ChaMode) -> impl Future>; + fn set_attrs(&self, attrs: Attrs) -> impl Future>; fn symlink(&self, original: S, _is_dir: F) -> impl Future> where diff --git a/yazi-fs/src/file/files.rs b/yazi-fs/src/file/files.rs index 62434be8..9e48b615 100644 --- a/yazi-fs/src/file/files.rs +++ b/yazi-fs/src/file/files.rs @@ -1,5 +1,6 @@ use std::ops::{Deref, DerefMut}; +use mlua::{FromLua, Lua, Value}; use yazi_shared::url::UrlBuf; use crate::file::File; @@ -28,3 +29,9 @@ impl From for Vec { impl From for Vec { fn from(value: Files) -> Self { value.0.into_iter().map(|f| f.url).collect() } } + +impl FromLua for Files { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + Vec::::from_lua(value, lua).map(Self) + } +} diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index 4772769f..61239627 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -36,7 +36,7 @@ pub fn max_common_root(urls: &[UrlBuf]) -> usize { .components() .zip(parent.components()) .take_while(|(a, b)| match (a, b) { - (Component::Scheme(a), Component::Scheme(b)) => a.covariant(*b), + (Component::Auth(a), Component::Auth(b)) => a.covariant(b), (a, b) => a == b, }) .count() @@ -53,6 +53,8 @@ pub fn max_common_root(urls: &[UrlBuf]) -> usize { #[cfg(unix)] #[test] fn test_max_common_root() { + yazi_shared::init_tests(); + fn assert(input: &[&str], expected: &str) { use std::{ffi::OsStr, str::FromStr}; let urls: Vec<_> = diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index d6532551..666cd11b 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,8 +1,8 @@ extern crate self as yazi_fs; -yazi_macro::mod_pub!(cha file mounts path provider); +yazi_macro::mod_pub!(cha file mounts path engine); -yazi_macro::mod_flat!(cwd entries filter fns hash op scheme sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(cwd spec entries filter fns hash op sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default()); diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index 8fa4085a..2fef639f 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -8,8 +8,8 @@ pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { cow.trail().components().count() - 1, ); - let scheme = cow.into_scheme().into_owned().with_ports(uri, urn); - (scheme, path).try_into().expect("UrlBuf from cleaned path") + let spec = cow.into_spec().with_ports(uri, urn); + (spec, path).try_into().expect("UrlBuf from cleaned path") } fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usize, usize) { @@ -76,19 +76,25 @@ mod tests { yazi_shared::init_tests(); let cases = [ // CurDir - ("archive://:3//./tmp/test.zip/foo/bar", "archive://:3//tmp/test.zip/foo/bar"), - ("archive://:3//tmp/./test.zip/foo/bar", "archive://:3//tmp/test.zip/foo/bar"), - ("archive://:3//tmp/./test.zip/./foo/bar", "archive://:3//tmp/test.zip/foo/bar"), - ("archive://:3//tmp/./test.zip/./foo/./bar/.", "archive://:3//tmp/test.zip/foo/bar"), + ("test-mount://7z:3//./tmp/test.zip/foo/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"), + ("test-mount://7z:3//tmp/./test.zip/foo/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"), + ("test-mount://7z:3//tmp/./test.zip/./foo/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"), + ( + "test-mount://7z:3//tmp/./test.zip/./foo/./bar/.", + "test-mount://7z:3//tmp/test.zip/foo/bar", + ), // ParentDir - ("archive://:3:2//../../tmp/test.zip/foo/bar", "archive://:3:2//tmp/test.zip/foo/bar"), - ("archive://:3:2//tmp/../../test.zip/foo/bar", "archive://:3:2//test.zip/foo/bar"), - ("archive://:4:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"), - ("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"), - ("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive:////tmp/test.zip"), - ("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:1//tmp/test.zip"), - ("archive://:4:4//tmp/test.zip/foo/bar/../../../", "archive:////tmp"), - ("sftp://test//root/.config/yazi/../../Downloads", "sftp://test//root/Downloads"), + ( + "test-mount://7z:3:2//../../tmp/test.zip/foo/bar", + "test-mount://7z:3:2//tmp/test.zip/foo/bar", + ), + ("test-mount://7z:3:2//tmp/../../test.zip/foo/bar", "test-mount://7z:3:2//test.zip/foo/bar"), + ("test-mount://7z:4:2//tmp/test.zip/../../foo/bar", "test-mount://7z:2:2//foo/bar"), + ("test-mount://7z:5:2//tmp/test.zip/../../foo/bar", "test-mount://7z:2:2//foo/bar"), + ("test-mount://7z:4:4//tmp/test.zip/foo/bar/../../", "test-mount://7z//tmp/test.zip"), + ("test-mount://7z:5:4//tmp/test.zip/foo/bar/../../", "test-mount://7z:1//tmp/test.zip"), + ("test-mount://7z:4:4//tmp/test.zip/foo/bar/../../../", "test-mount://7z//tmp"), + ("sftp://vps//root/.config/yazi/../../Downloads", "sftp://vps//root/Downloads"), ]; for (input, expected) in cases { diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index b5a49027..18d95794 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathKind, PathLike}, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use yazi_shim::wtf8::FromWtf8Vec; #[inline] @@ -32,17 +32,21 @@ fn expand_url_impl(url: UrlCow) -> UrlCow { Url::Regular(_) => UrlBuf::Regular( LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), ), - Url::Search { domain, .. } => UrlBuf::Search { - loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), - domain: domain.intern(), + Url::Search { auth, .. } => UrlBuf::Search { + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + auth: auth.clone(), }, - Url::Archive { domain, .. } => UrlBuf::Archive { - loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), - domain: domain.intern(), + Url::Mount { auth, .. } => UrlBuf::Mount { + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + auth: auth.clone(), }, - Url::Sftp { domain, .. } => UrlBuf::Sftp { - loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), - domain: domain.intern(), + Url::Scope { auth, .. } => UrlBuf::Scope { + loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), + auth: auth.clone(), + }, + Url::Sftp { auth, .. } => UrlBuf::Sftp { + loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), + auth: auth.clone(), }, } .into() @@ -94,39 +98,48 @@ mod tests { let cases = [ // Zero extra component expanded - ("archive:////tmp/test.zip/$FOO/bar", "archive:////tmp/test.zip/foo/bar"), - ("archive://:1//tmp/test.zip/$FOO/bar", "archive://:1//tmp/test.zip/foo/bar"), - ("archive://:2//tmp/test.zip/bar/$FOO", "archive://:2//tmp/test.zip/bar/foo"), - ("archive://:3//tmp/test.zip/$FOO/bar", "archive://:3//tmp/test.zip/foo/bar"), - ("archive://:3:1//tmp/test.zip/bar/$FOO", "archive://:3:1//tmp/test.zip/bar/foo"), - ("archive://:3:2//tmp/test.zip/$FOO/bar", "archive://:3:2//tmp/test.zip/foo/bar"), - ("archive://:3:3//tmp/test.zip/bar/$FOO", "archive://:3:3//tmp/test.zip/bar/foo"), + ("test-mount://7z//tmp/test.zip/$FOO/bar", "test-mount://7z//tmp/test.zip/foo/bar"), + ("test-mount://7z:1//tmp/test.zip/$FOO/bar", "test-mount://7z:1//tmp/test.zip/foo/bar"), + ("test-mount://7z:2//tmp/test.zip/bar/$FOO", "test-mount://7z:2//tmp/test.zip/bar/foo"), + ("test-mount://7z:3//tmp/test.zip/$FOO/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"), + ("test-mount://7z:3:1//tmp/test.zip/bar/$FOO", "test-mount://7z:3:1//tmp/test.zip/bar/foo"), + ("test-mount://7z:3:2//tmp/test.zip/$FOO/bar", "test-mount://7z:3:2//tmp/test.zip/foo/bar"), + ("test-mount://7z:3:3//tmp/test.zip/bar/$FOO", "test-mount://7z:3:3//tmp/test.zip/bar/foo"), // +1 component - ("archive:////tmp/test.zip/$BAR_BAZ", "archive:////tmp/test.zip/bar/baz"), - ("archive://:1//tmp/test.zip/$BAR_BAZ", "archive://:2//tmp/test.zip/bar/baz"), - ("archive://:2//$BAR_BAZ/tmp/test.zip", "archive://:2//bar/baz/tmp/test.zip"), - ("archive://:2:1//tmp/test.zip/$BAR_BAZ", "archive://:3:2//tmp/test.zip/bar/baz"), - ("archive://:2:2//tmp/$BAR_BAZ/test.zip", "archive://:3:3//tmp/bar/baz/test.zip"), - ("archive://:2:2//$BAR_BAZ/tmp/test.zip", "archive://:2:2//bar/baz/tmp/test.zip"), + ("test-mount://7z//tmp/test.zip/$BAR_BAZ", "test-mount://7z//tmp/test.zip/bar/baz"), + ("test-mount://7z:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:2//tmp/test.zip/bar/baz"), + ("test-mount://7z:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2//bar/baz/tmp/test.zip"), + ("test-mount://7z:2:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:3:2//tmp/test.zip/bar/baz"), + ("test-mount://7z:2:2//tmp/$BAR_BAZ/test.zip", "test-mount://7z:3:3//tmp/bar/baz/test.zip"), + ("test-mount://7z:2:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2:2//bar/baz/tmp/test.zip"), // -1 component - ("archive:////tmp/test.zip/${BAR/BAZ}", "archive:////tmp/test.zip/bar_baz"), - ("archive://:1//tmp/test.zip/${BAR/BAZ}", "archive://:1//tmp/test.zip/${BAR/BAZ}"), - ("archive://:1//tmp/${BAR/BAZ}/test.zip", "archive://:1//tmp/bar_baz/test.zip"), - ("archive://:2//tmp/test.zip/${BAR/BAZ}", "archive://:1//tmp/test.zip/bar_baz"), - ("archive://:2//tmp/${BAR/BAZ}/test.zip", "archive://:2//tmp/${BAR/BAZ}/test.zip"), - ("archive://:2:1//tmp/test.zip/${BAR/BAZ}", "archive://:2:1//tmp/test.zip/${BAR/BAZ}"), - ("archive://:2:1//tmp/${BAR/BAZ}/test.zip", "archive://:2:1//tmp/${BAR/BAZ}/test.zip"), - ("archive://:2:1//${BAR/BAZ}/tmp/test.zip", "archive://:2:1//bar_baz/tmp/test.zip"), - ("archive://:3:2//tmp/test.zip/${BAR/BAZ}", "archive://:2:1//tmp/test.zip/bar_baz"), - ("archive://:3:2//tmp/${BAR/BAZ}/test.zip", "archive://:3:2//tmp/${BAR/BAZ}/test.zip"), - ("archive://:3:3//tmp/test.zip/${BAR/BAZ}", "archive://:2:2//tmp/test.zip/bar_baz"), - ("archive://:3:3//tmp/${BAR/BAZ}/test.zip", "archive://:2:2//tmp/bar_baz/test.zip"), + ("test-mount://7z//tmp/test.zip/${BAR/BAZ}", "test-mount://7z//tmp/test.zip/bar_baz"), + ("test-mount://7z:1//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/${BAR/BAZ}"), + ("test-mount://7z:1//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:1//tmp/bar_baz/test.zip"), + ("test-mount://7z:2//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/bar_baz"), + ("test-mount://7z:2//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2//tmp/${BAR/BAZ}/test.zip"), + ( + "test-mount://7z:2:1//tmp/test.zip/${BAR/BAZ}", + "test-mount://7z:2:1//tmp/test.zip/${BAR/BAZ}", + ), + ( + "test-mount://7z:2:1//tmp/${BAR/BAZ}/test.zip", + "test-mount://7z:2:1//tmp/${BAR/BAZ}/test.zip", + ), + ("test-mount://7z:2:1//${BAR/BAZ}/tmp/test.zip", "test-mount://7z:2:1//bar_baz/tmp/test.zip"), + ("test-mount://7z:3:2//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:1//tmp/test.zip/bar_baz"), + ( + "test-mount://7z:3:2//tmp/${BAR/BAZ}/test.zip", + "test-mount://7z:3:2//tmp/${BAR/BAZ}/test.zip", + ), + ("test-mount://7z:3:3//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:2//tmp/test.zip/bar_baz"), + ("test-mount://7z:3:3//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2:2//tmp/bar_baz/test.zip"), // Zeros all components - ("archive:////${EM/PT/Y}", "archive:////"), - ("archive://:1//${EM/PT/Y}", "archive://:1//${EM/PT/Y}"), - ("archive://:2//${EM/PT/Y}", "archive://:2//${EM/PT/Y}"), - ("archive://:3//${EM/PT/Y}", "archive:////"), - ("archive://:4//${EM/PT/Y}", "archive://:1//"), + ("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"), + ("test-mount://7z:1//${EM/PT/Y}", "test-mount://7z:1//${EM/PT/Y}"), + ("test-mount://7z:2//${EM/PT/Y}", "test-mount://7z:2//${EM/PT/Y}"), + ("test-mount://7z:3//${EM/PT/Y}", "test-mount://7z//"), + ("test-mount://7z:4//${EM/PT/Y}", "test-mount://7z:1//"), ]; for (input, expected) in cases { diff --git a/yazi-fs/src/path/normalize.rs b/yazi-fs/src/path/normalize.rs index 13cb152c..a76ed93f 100644 --- a/yazi-fs/src/path/normalize.rs +++ b/yazi-fs/src/path/normalize.rs @@ -5,7 +5,7 @@ use yazi_shared::url::UrlBuf; use crate::path::{clean_url, expand_url}; pub fn sanitize_path(path: PathBuf) -> Option { - clean_url(yazi_fs::provider::local::try_absolute(expand_url(UrlBuf::from(path)))?) + clean_url(yazi_fs::engine::local::try_absolute(expand_url(UrlBuf::from(path)))?) .into_loc() .into_os() .ok() diff --git a/yazi-fs/src/provider/capabilities.rs b/yazi-fs/src/provider/capabilities.rs deleted file mode 100644 index 323bbf95..00000000 --- a/yazi-fs/src/provider/capabilities.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[derive(Clone, Copy, Debug)] -pub struct Capabilities { - pub symlink: bool, -} diff --git a/yazi-fs/src/provider/mod.rs b/yazi-fs/src/provider/mod.rs deleted file mode 100644 index 1a96a542..00000000 --- a/yazi-fs/src/provider/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -yazi_macro::mod_pub!(local); - -yazi_macro::mod_flat!(attrs capabilities traits); diff --git a/yazi-fs/src/scheme.rs b/yazi-fs/src/scheme.rs deleted file mode 100644 index 415a0252..00000000 --- a/yazi-fs/src/scheme.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::path::PathBuf; - -use yazi_shared::{path::PathBufDyn, scheme::{AsScheme, Scheme, SchemeInventory, SchemeRef}}; -use yazi_shim::mlua::UserDataFieldsExt; - -use crate::Xdg; - -pub trait FsScheme { - fn cache(&self) -> Option; -} - -impl FsScheme for SchemeRef<'_> { - fn cache(&self) -> Option { - match self { - Self::Regular { .. } | Self::Search { .. } => None, - Self::Archive { domain, .. } => Some( - Xdg::temp_dir().join(format!("archive-{}", yazi_shared::scheme::Encode::domain(domain))), - ), - Self::Sftp { domain, .. } => { - Some(Xdg::temp_dir().join(format!("sftp-{}", yazi_shared::scheme::Encode::domain(domain)))) - } - } - } -} - -impl FsScheme for Scheme { - fn cache(&self) -> Option { self.as_scheme().cache() } -} - -// --- Inject -inventory::submit! { - SchemeInventory { - register: |registry| { - registry.add_cached_field("cache", |_, me| Ok(me.cache().map(PathBufDyn::from))); - } - } -} diff --git a/yazi-fs/src/spec.rs b/yazi-fs/src/spec.rs new file mode 100644 index 00000000..1af5029a --- /dev/null +++ b/yazi-fs/src/spec.rs @@ -0,0 +1,33 @@ +use std::path::PathBuf; + +use yazi_shared::{auth::{Auth, AuthKind, Encode}, path::PathBufDyn, spec::SpecInventory}; +use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; + +use crate::Xdg; + +pub trait FsSpec { + fn cache(&self) -> Option; +} + +impl FsSpec for Auth { + fn cache(&self) -> Option { + match self.kind { + AuthKind::Regular | AuthKind::Search => None, + AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Some(Xdg::temp_dir().join(format!( + "{}_{}_{}", + self.kind.into_str(), + self.scheme, + Encode::domain(&self.domain) + ))), + } + } +} + +// --- Inject +inventory::submit! { + SpecInventory { + register: |registry| { + registry.add_cached_field("cache", |_, me| Ok(me.cache().map(PathBufDyn::from))); + } + } +} diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index 80d17a9a..32f14201 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,9 +1,10 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow}}; +use mlua::UserDataFields; +use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; use yazi_shim::mlua::UserDataFieldsExt; -use crate::{FsHash128, FsScheme, path::PercentEncoding}; +use crate::{FsHash128, FsSpec, path::PercentEncoding}; pub trait FsUrl<'a> { fn cache(&self) -> Option; @@ -36,11 +37,11 @@ impl<'a> FsUrl<'a> for Url<'a> { root } - self.scheme().cache().map(|root| with_loc(self.loc(), root)) + self.auth().cache().map(|root| with_loc(self.loc(), root)) } fn cache_lock(&self) -> Option { - self.scheme().cache().map(|mut root| { + self.auth().cache().map(|mut root| { root.push("%lock"); root.push(format!("{:x}", self.hash_u128())); root @@ -50,7 +51,7 @@ impl<'a> FsUrl<'a> for Url<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), - Self::Archive { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -65,7 +66,7 @@ impl FsUrl<'_> for UrlBuf { fn unified_path(self) -> Cow<'static, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), - Self::Archive { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -80,7 +81,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(), - Self::Archive { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -92,6 +93,22 @@ inventory::submit! { UrlBufInventory { register: |registry| { registry.add_cached_field("cache", |_, me| Ok(me.cache())); + registry.add_cached_field("domain", |lua, me| { + yazi_binding::deprecate!(lua, "{}: `Url.domain` is deprecated, use `Url.spec.domain` instead."); + lua.create_string(&*me.spec().domain) + }); + registry.add_field_method_get("is_regular", |lua, me| { + yazi_binding::deprecate!(lua, "{}: `Url.is_regular` is deprecated, use `Url.spec.is_regular` instead."); + Ok(me.is_regular()) + }); + registry.add_field_method_get("is_search", |lua, me| { + yazi_binding::deprecate!(lua, "{}: `Url.is_search` is deprecated, use `Url.spec.is_search` instead."); + Ok(me.is_search()) + }); + registry.add_cached_field("scheme", |lua, me| { + yazi_binding::deprecate!(lua, "{}: `Url.scheme` is deprecated, use `Url.spec` instead."); + Ok(me.spec()) + }); } } } diff --git a/yazi-parser/src/mgr/cd.rs b/yazi-parser/src/mgr/cd.rs index 9b310531..484d2bb2 100644 --- a/yazi-parser/src/mgr/cd.rs +++ b/yazi-parser/src/mgr/cd.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use yazi_core::mgr::CdSource; use yazi_fs::path::{clean_url, expand_url}; use yazi_shared::{event::ActionCow, url::{Url, UrlBuf}}; -use yazi_vfs::provider; +use yazi_vfs::engine; #[derive(Debug, Deserialize)] pub struct CdForm { @@ -27,7 +27,7 @@ impl TryFrom for CdForm { me.target = expand_url(me.target).into_owned(); } - if let Some(u) = provider::try_absolute(&me.target) + if let Some(u) = engine::try_absolute(&me.target) && u.is_owned() { me.target = u.into_owned(); diff --git a/yazi-parser/src/mgr/reveal.rs b/yazi-parser/src/mgr/reveal.rs index 133f9896..93f8c003 100644 --- a/yazi-parser/src/mgr/reveal.rs +++ b/yazi-parser/src/mgr/reveal.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use yazi_core::mgr::CdSource; use yazi_fs::path::{clean_url, expand_url}; use yazi_shared::{event::ActionCow, url::UrlBuf}; -use yazi_vfs::provider; +use yazi_vfs::engine; #[derive(Debug, Deserialize)] pub struct RevealForm { @@ -27,7 +27,7 @@ impl TryFrom for RevealForm { me.target = expand_url(me.target).into_owned(); } - if let Some(u) = provider::try_absolute(&me.target) + if let Some(u) = engine::try_absolute(&me.target) && u.is_owned() { me.target = u.into_owned(); diff --git a/yazi-parser/src/mgr/tab_create.rs b/yazi-parser/src/mgr/tab_create.rs index 20c7090f..1956266e 100644 --- a/yazi-parser/src/mgr/tab_create.rs +++ b/yazi-parser/src/mgr/tab_create.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use yazi_boot::BOOT; use yazi_fs::path::{clean_url, expand_url}; use yazi_shared::{event::ActionCow, url::UrlBuf}; -use yazi_vfs::provider; +use yazi_vfs::engine; #[derive(Debug, Deserialize)] pub struct TabCreateForm { @@ -30,7 +30,7 @@ impl TryFrom for TabCreateForm { target = expand_url(target).into_owned(); } - if let Some(u) = provider::try_absolute(&target) + if let Some(u) = engine::try_absolute(&target) && u.is_owned() { target = u.into_owned(); diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index 494d06a4..8ae0018b 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -37,8 +37,8 @@ function Header:flags() local finder = self._tab.finder local t = {} - if cwd.is_search then - t[#t + 1] = string.format("search: %s", cwd.domain) + if cwd.spec.is_search then + t[#t + 1] = string.format("search: %s", cwd.spec.domain) end if filter then t[#t + 1] = string.format("filter: %s", filter) diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index 8d5552e3..6cd4fb8e 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -12,7 +12,7 @@ function M:entry() ya.emit("escape", { visual = true }) local cwd, selected = state() - if cwd.scheme.is_virtual then + if cwd.spec.is_virtual then return ya.notify { title = "Fzf", content = "Not supported under virtual filesystems", timeout = 5, level = "warn" } end diff --git a/yazi-plugin/preset/plugins/mime-dir.lua b/yazi-plugin/preset/plugins/mime-dir.lua index 38abab94..6bb204b4 100644 --- a/yazi-plugin/preset/plugins/mime-dir.lua +++ b/yazi-plugin/preset/plugins/mime-dir.lua @@ -1,7 +1,7 @@ local function fetch(_, job) local updates = {} for _, file in ipairs(job.files) do - if file.url.scheme.is_virtual then + if file.url.spec.is_virtual then updates[file.url] = "folder/remote" else updates[file.url] = "folder/local" diff --git a/yazi-plugin/preset/plugins/mime-remote.lua b/yazi-plugin/preset/plugins/mime-remote.lua index 4654940f..2bd474d8 100644 --- a/yazi-plugin/preset/plugins/mime-remote.lua +++ b/yazi-plugin/preset/plugins/mime-remote.lua @@ -2,7 +2,7 @@ local M = {} local function stale_cache(file) local url = file.url - local lock = url.scheme.cache:join(string.format("%%lock/%s", url:hash(true))) + local lock = url.spec.cache:join(string.format("%%lock/%s", url:hash(true))) local fd = fs.access():read(true):open(Url(lock)) if not fd then diff --git a/yazi-plugin/src/fs/calculator.rs b/yazi-plugin/src/fs/calculator.rs index 9c9a5b83..294c01ab 100644 --- a/yazi-plugin/src/fs/calculator.rs +++ b/yazi-plugin/src/fs/calculator.rs @@ -2,8 +2,8 @@ use mlua::{IntoLuaMulti, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::Error; pub enum SizeCalculator { - Local(yazi_fs::provider::local::SizeCalculator), - Remote(yazi_vfs::provider::SizeCalculator), + Local(yazi_fs::engine::local::SizeCalculator), + Remote(yazi_vfs::engine::SizeCalculator), } impl UserData for SizeCalculator { diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 404b6b2b..5f6ca642 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -3,9 +3,9 @@ use std::str::FromStr; use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, LuaString, Table, Value}; use yazi_binding::{Composer, ComposerGet, ComposerSet, Error}; use yazi_config::Pattern; -use yazi_fs::{file::File, mounts::PARTITIONS, provider::{Attrs, DirReader, FileHolder}}; +use yazi_fs::{engine::{Attrs, DirReader, FileHolder}, file::File, mounts::PARTITIONS}; use yazi_shared::url::{UrlBuf, UrlCow, UrlLike, UrlRef}; -use yazi_vfs::{VfsFile, provider}; +use yazi_vfs::{VfsFile, engine}; use crate::fs::SizeCalculator; @@ -38,15 +38,15 @@ pub fn compose() -> Composer { } fn access(lua: &Lua) -> mlua::Result { - lua.create_function(|_, ()| Ok(yazi_vfs::provider::Gate::default())) + lua.create_function(|_, ()| Ok(yazi_vfs::engine::Demand::default())) } fn calc_size(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { let it = if let Some(path) = url.as_local() { - yazi_fs::provider::local::SizeCalculator::new(path).await.map(SizeCalculator::Local) + yazi_fs::engine::local::SizeCalculator::new(path).await.map(SizeCalculator::Local) } else { - yazi_vfs::provider::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote) + yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote) }; match it { @@ -59,9 +59,9 @@ fn calc_size(lua: &Lua) -> mlua::Result { fn cha(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, follow): (UrlRef, Option)| async move { let cha = if follow.unwrap_or(false) { - provider::metadata(&*url).await + engine::metadata(&*url).await } else { - provider::symlink_metadata(&*url).await + engine::symlink_metadata(&*url).await }; match cha { @@ -73,7 +73,7 @@ fn cha(lua: &Lua) -> mlua::Result { fn copy(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move { - match provider::copy(&*from, &*to, Attrs::default()).await { + match engine::copy(&*from, &*to, Attrs::default()).await { Ok(len) => len.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } @@ -83,8 +83,8 @@ fn copy(lua: &Lua) -> mlua::Result { fn create(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move { let result = match &*r#type.as_bytes() { - b"dir" => provider::create_dir(&*url).await, - b"dir_all" => provider::create_dir_all(&*url).await, + b"dir" => engine::create_dir(&*url).await, + b"dir_all" => engine::create_dir_all(&*url).await, _ => Err("Creation type must be 'dir' or 'dir_all'".into_lua_err())?, }; @@ -171,7 +171,7 @@ fn read_dir(lua: &Lua) -> mlua::Result { let limit = options.raw_get("limit").unwrap_or(usize::MAX); let resolve = options.raw_get::("resolve")?; - let mut it = match provider::read_dir(&*dir).await { + let mut it = match engine::read_dir(&*dir).await { Ok(it) => it, Err(e) => return (Value::Nil, Error::Io(e)).into_lua_multi(&lua), }; @@ -204,10 +204,10 @@ fn read_dir(lua: &Lua) -> mlua::Result { fn remove(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move { let result = match &*r#type.as_bytes() { - 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" => provider::remove_dir_clean(&*url).await, + b"file" => engine::remove_file(&*url).await, + b"dir" => engine::remove_dir(&*url).await, + b"dir_all" => engine::remove_dir_all(&*url).await, + b"dir_clean" => engine::remove_dir_clean(&*url).await, _ => Err("Removal type must be 'file', 'dir', 'dir_all', or 'dir_clean'".into_lua_err())?, }; @@ -220,7 +220,7 @@ fn remove(lua: &Lua) -> mlua::Result { fn rename(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move { - match provider::rename(&*from, &*to).await { + match engine::rename(&*from, &*to).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), } @@ -244,7 +244,7 @@ fn unique(lua: &Lua) -> mlua::Result { fn write(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, data): (UrlRef, LuaString)| async move { - match provider::write(&*url, data.as_bytes()).await { + match engine::write(&*url, data.as_bytes()).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), } diff --git a/yazi-plugin/src/ui/ui.rs b/yazi-plugin/src/ui/ui.rs index c8a8c010..dc81a2f2 100644 --- a/yazi-plugin/src/ui/ui.rs +++ b/yazi-plugin/src/ui/ui.rs @@ -1,7 +1,10 @@ use mlua::{IntoLua, Lua, Value}; use yazi_binding::{Composer, ComposerGet, ComposerSet, elements::{Align, Bar, Border, Color, Constraint, Edge, Fill, Gauge, Layout, Line, List, Pad, Rect, Row, Span, Table, Text, Wrap}, position::Position, style::Style}; use yazi_config::THEME; -use yazi_widgets::{clear::Clear, input::InputArc}; +use yazi_dds::Pubsub; +use yazi_macro::err; +use yazi_shim::strum::IntoStr; +use yazi_widgets::{clear::Clear, input::{InputArc, InputEvent}}; pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { @@ -16,7 +19,7 @@ pub fn compose() -> Composer { b"Edge" => Edge::compose(lua)?, b"Fill" => Fill::compose(lua)?, b"Gauge" => Gauge::compose(lua)?, - b"Input" => InputArc::compose(lua, (&THEME.input).into())?, + b"Input" => InputArc::compose(lua, (&THEME.input).into(), publish_input)?, b"Layout" => Layout::compose(lua)?, b"Line" => Line::compose(lua)?, b"List" => List::compose(lua)?, @@ -48,3 +51,7 @@ pub fn compose() -> Composer { Composer::new(get, set) } + +fn publish_input(event: InputEvent) { + err!(Pubsub::pub_after_input((&event).into_str(), event.value())); +} diff --git a/yazi-plugin/src/utils/app.rs b/yazi-plugin/src/utils/app.rs index d3d0a431..926e606f 100644 --- a/yazi-plugin/src/utils/app.rs +++ b/yazi-plugin/src/utils/app.rs @@ -2,7 +2,7 @@ use std::any::TypeId; use mlua::{AnyUserData, ExternalError, Function, Lua, LuaString}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; -use yazi_vfs::provider::RwFile; +use yazi_vfs::engine::RwFile; use super::Utils; diff --git a/yazi-plugin/src/utils/sync.rs b/yazi-plugin/src/utils/sync.rs index 0de4dfa1..ea155bb4 100644 --- a/yazi-plugin/src/utils/sync.rs +++ b/yazi-plugin/src/utils/sync.rs @@ -95,7 +95,7 @@ impl Utils { } (b"mpsc", Some(buffer)) => { let (tx, rx) = tokio::sync::mpsc::channel::(buffer); - (MpscTx(tx), MpscRx(rx)).into_lua_multi(lua) + (MpscTx::new(tx), MpscRx(rx)).into_lua_multi(lua) } (b"mpsc", None) => { let (tx, rx) = tokio::sync::mpsc::unbounded_channel::(); diff --git a/yazi-runner/Cargo.toml b/yazi-runner/Cargo.toml index 29506f35..951ad92f 100644 --- a/yazi-runner/Cargo.toml +++ b/yazi-runner/Cargo.toml @@ -30,6 +30,7 @@ anyhow = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } parking_lot = { workspace = true } +strum = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } diff --git a/yazi-runner/src/lib.rs b/yazi-runner/src/lib.rs index 82e8db1d..9251a61e 100644 --- a/yazi-runner/src/lib.rs +++ b/yazi-runner/src/lib.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_pub!(entry fetcher loader preloader previewer); +yazi_macro::mod_pub!(entry fetcher loader preloader previewer provider); yazi_macro::mod_flat!(runner spot); @@ -6,6 +6,5 @@ pub static RUNNER: yazi_shim::cell::RoCell = yazi_shim::cell::RoCell::ne pub fn init(setter: fn(&mlua::Lua) -> mlua::Result<()>) { crate::loader::init(); - RUNNER.init(Runner { setter }); } diff --git a/yazi-runner/src/loader/loader.rs b/yazi-runner/src/loader/loader.rs index 6d118786..e4593b46 100644 --- a/yazi-runner/src/loader/loader.rs +++ b/yazi-runner/src/loader/loader.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail, ensure}; use hashbrown::HashMap; use mlua::{ExternalError, Lua, Table, chunk::ChunkMode}; use parking_lot::RwLock; -use yazi_fs::{Xdg, provider::local::Local}; +use yazi_fs::{Xdg, engine::local::Local}; use yazi_macro::plugin_preset as preset; use yazi_shared::{BytesExt, LOG_LEVEL}; use yazi_shim::cell::RoCell; diff --git a/yazi-runner/src/provider/job.rs b/yazi-runner/src/provider/job.rs new file mode 100644 index 00000000..79abdc58 --- /dev/null +++ b/yazi-runner/src/provider/job.rs @@ -0,0 +1,162 @@ +use std::io; + +use mlua::{IntoLua, Lua, Value}; +use strum::AsRefStr; +use yazi_binding::MpscTx; +use yazi_fs::engine::{Attrs, Demand}; +use yazi_shared::{path::PathBufDyn, url::UrlBuf}; + +#[derive(AsRefStr)] +#[strum(serialize_all = "PascalCase")] +pub enum ProviderJob { + Capabilities, + Absolute { + url: UrlBuf, + }, + Canonicalize { + url: UrlBuf, + }, + Casefold { + url: UrlBuf, + }, + SymlinkMetadata { + url: UrlBuf, + }, + Metadata { + url: UrlBuf, + }, + ReadDir { + url: UrlBuf, + }, + Open { + url: UrlBuf, + attrs: Attrs, + demand: Demand, + }, + CreateDir { + url: UrlBuf, + }, + HardLink { + from: UrlBuf, + to: PathBufDyn, + }, + ReadLink { + url: UrlBuf, + }, + RemoveDir { + url: UrlBuf, + }, + RemoveFile { + url: UrlBuf, + }, + Rename { + from: UrlBuf, + to: PathBufDyn, + }, + Symlink { + original: Vec, + url: UrlBuf, + is_dir: bool, + }, + Trash { + url: UrlBuf, + }, + Read { + url: UrlBuf, + offset: u64, + len: usize, + }, + Write { + url: UrlBuf, + offset: u64, + bytes: Vec, + }, + Copy { + from: UrlBuf, + to: PathBufDyn, + attrs: Attrs, + }, + CopyProgressive { + from: UrlBuf, + to: PathBufDyn, + attrs: Attrs, + tx: MpscTx>, + }, + SetLen { + url: UrlBuf, + size: u64, + }, + SetAttrs { + url: UrlBuf, + attrs: Attrs, + }, +} + +impl IntoLua for ProviderJob { + fn into_lua(self, lua: &Lua) -> mlua::Result { + let t = lua.create_table()?; + t.raw_set("op", self.as_ref())?; + + match self { + Self::Capabilities => {} + + Self::Absolute { url } + | Self::Canonicalize { url } + | Self::Casefold { url } + | Self::SymlinkMetadata { url } + | Self::Metadata { url } + | Self::ReadDir { url } + | Self::CreateDir { url } + | Self::ReadLink { url } + | Self::RemoveDir { url } + | Self::RemoveFile { url } + | Self::Trash { url } => t.raw_set("url", url)?, + + Self::Open { url, attrs, demand } => { + t.raw_set("url", url)?; + t.raw_set("attrs", attrs)?; + t.raw_set("demand", demand)?; + } + Self::HardLink { from, to } | Self::Rename { from, to } => { + t.raw_set("from", from)?; + t.raw_set("to", to)?; + } + Self::Symlink { original, url, is_dir } => { + t.raw_set("original", lua.create_external_string(original)?)?; + t.raw_set("url", url)?; + t.raw_set("is_dir", is_dir)?; + } + Self::Read { url, offset, len } => { + t.raw_set("url", url)?; + t.raw_set("offset", offset)?; + t.raw_set("len", len)?; + } + Self::Write { url, offset, bytes } => { + t.raw_set("url", url)?; + t.raw_set("offset", offset)?; + t.raw_set("bytes", lua.create_external_string(bytes)?)?; + } + Self::Copy { from, to, attrs } => { + t.raw_set("from", from)?; + t.raw_set("to", to)?; + t.raw_set("attrs", attrs)?; + } + Self::CopyProgressive { from, to, attrs, tx } => { + t.raw_set("from", from)?; + t.raw_set("to", to)?; + t.raw_set("attrs", attrs)?; + t.raw_set("tx", tx)?; + } + Self::SetLen { url, size } => { + t.raw_set("url", url)?; + t.raw_set("size", size)?; + } + Self::SetAttrs { url, attrs } => { + t.raw_set("url", url)?; + t.raw_set("attrs", attrs)?; + } + } + + t.into_lua(lua) + } +} diff --git a/yazi-runner/src/provider/mod.rs b/yazi-runner/src/provider/mod.rs new file mode 100644 index 00000000..70b63fea --- /dev/null +++ b/yazi-runner/src/provider/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(job result provider); diff --git a/yazi-runner/src/provider/provider.rs b/yazi-runner/src/provider/provider.rs new file mode 100644 index 00000000..71642884 --- /dev/null +++ b/yazi-runner/src/provider/provider.rs @@ -0,0 +1,42 @@ +use mlua::{ExternalError, FromLua, FromLuaMulti, IntoLua, ObjectLike, Value}; +use tokio::runtime::Handle; +use yazi_shared::{data::Sendable, event::Cmd}; + +use crate::{Runner, loader::LOADER, provider::{ProvideResult, ProviderJob}}; + +impl Runner { + pub async fn provide(&'static self, run: &'static Cmd, job: ProviderJob) -> ProvideResult + where + T: FromLua + Send + 'static, + { + match LOADER.ensure(&run.name, |_| ()).await { + Ok(()) => self.provide_do(run, job).await, + Err(e) => yazi_binding::Error::custom(e.to_string()).into(), + } + } + + async fn provide_do(&'static self, run: &'static Cmd, job: ProviderJob) -> ProvideResult + where + T: FromLua + Send + 'static, + { + match tokio::task::spawn_blocking(move || { + let lua = self.spawn(&run.name)?; + + Handle::current().block_on(async { + let Value::Table(job) = job.into_lua(&lua)? else { + return Err("ProviderJob should be a table".into_lua_err()); + }; + job.raw_set("args", Sendable::args_to_table_ref(&lua, &run.args)?)?; + + let values = LOADER.load(&lua, &run.name).await?.call_async_method("provide", job).await?; + ProvideResult::from_lua_multi(values, &lua) + }) + }) + .await + { + Ok(Ok(result)) => result, + Ok(Err(error)) => error.into(), + Err(error) => error.into(), + } + } +} diff --git a/yazi-runner/src/provider/result.rs b/yazi-runner/src/provider/result.rs new file mode 100644 index 00000000..f8ef92e6 --- /dev/null +++ b/yazi-runner/src/provider/result.rs @@ -0,0 +1,43 @@ +use mlua::{FromLua, FromLuaMulti, Lua, MultiValue, Value}; + +pub struct ProvideResult(pub Result); + +impl From for ProvideResult { + fn from(value: yazi_binding::Error) -> Self { Self(Err(value)) } +} + +impl From for ProvideResult { + fn from(value: mlua::Error) -> Self { yazi_binding::Error::custom(value.to_string()).into() } +} + +impl From for ProvideResult { + fn from(value: tokio::task::JoinError) -> Self { + yazi_binding::Error::custom(value.to_string()).into() + } +} + +impl FromLuaMulti for ProvideResult { + fn from_lua_multi(mut values: MultiValue, lua: &Lua) -> mlua::Result { + let value = values.pop_front().unwrap_or(Value::Nil); + let error = values.pop_front().unwrap_or(Value::Nil); + + Ok(Self(if error.is_nil() { + T::from_lua(value, lua).map_err(|e| yazi_binding::Error::custom(e.to_string())) + } else { + Err( + yazi_binding::Error::from_lua(error, lua) + .unwrap_or_else(|e| yazi_binding::Error::custom(e.to_string())), + ) + })) + } +} + +impl ProvideResult { + pub fn ok(self) -> Result<(), yazi_binding::Error> { + if self.0? { + Ok(()) + } else { + Err(yazi_binding::Error::custom("Lua VFS returned false without an Error")) + } + } +} diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 500dce13..5141f54c 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -4,9 +4,9 @@ use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::NotFound}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::path_relative_to, provider::{Attrs, FileHolder, Provider, local::Local}}; +use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, engine::{Attrs, Engine, FileHolder, local::Local}, ok_or_not_found, path::path_relative_to}; use yazi_shared::{path::PathCow, url::{AsUrl, UrlCow, UrlLike}}; -use yazi_vfs::{VfsCha, maybe_exists, provider::{self, DirEntry}, unique_file}; +use yazi_vfs::{VfsCha, engine::{self, DirEntry}, maybe_exists, unique_file}; use super::{FileInCopy, FileInDelete, FileInHardlink, FileInLink, FileInTrash}; use crate::{LOW, NORMAL, TaskOp, TaskOps, TasksProxy, ctx, file::{FileIn, FileInCut, FileInDownload, FileInUpload, FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo, Transaction, Traverse}, hook::{HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink}, ok_or_not_found}; @@ -38,7 +38,7 @@ impl File { super::traverse::( task, - async |dir| match provider::create_dir(dir).await { + async |dir| match engine::create_dir(dir).await { Err(e) if e.kind() != io::ErrorKind::AlreadyExists => Err(e)?, _ => Ok(()), }, @@ -63,7 +63,7 @@ impl File { pub(crate) async fn copy_do(&self, mut task: FileInCopy) -> Result<(), FileOutCopyDo> { ok_or_not_found!(task, Transaction::unlink(&task.to).await); let mut rx = - ctx!(task, provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await)?; + ctx!(task, engine::copy_progressive(&task.from, &task.to, task.cha.unwrap()).await)?; loop { match rx.recv().await.unwrap_or(Ok(0)) { @@ -101,16 +101,16 @@ impl File { self.ops.out(id, HookInOutCut::new(&task.from, &task.to)); TasksProxy::update_succeed(id, [&task.to], true); - if !task.follow && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() { + if !task.follow && ok_or_not_found(engine::rename(&task.from, &task.to).await).is_ok() { return Ok(self.ops.out(id, FileOutCut::Succ)); } let (mut links, mut files) = (vec![], vec![]); - let reorder = task.follow && ctx!(task, provider::capabilities(&task.from).await)?.symlink; + let reorder = task.follow && ctx!(task, engine::capabilities(&task.from).await)?.symlink; super::traverse::( task, - async |dir| match provider::create_dir(dir).await { + async |dir| match engine::create_dir(dir).await { Err(e) if e.kind() != io::ErrorKind::AlreadyExists => Err(e)?, _ => Ok(()), }, @@ -155,12 +155,12 @@ impl File { pub(crate) async fn cut_do(&self, mut task: FileInCut) -> Result<(), FileOutCutDo> { ok_or_not_found!(task, Transaction::unlink(&task.to).await); let mut rx = - ctx!(task, provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await)?; + ctx!(task, engine::copy_progressive(&task.from, &task.to, task.cha.unwrap()).await)?; loop { match rx.recv().await.unwrap_or(Ok(0)) { Ok(0) => { - provider::remove_file(&task.from).await.ok(); + engine::remove_file(&task.from).await.ok(); break; } Ok(n) => self.ops.out(task.id, FileOutCutDo::Adv(n)), @@ -199,7 +199,7 @@ impl File { let mut src: PathCow = if task.resolve { ok_or_not_found!( task, - provider::read_link(&task.from).await, + engine::read_link(&task.from).await, return Ok(self.ops.out(task.id, FileOutLink::Succ)) ) .into() @@ -208,14 +208,14 @@ impl File { }; if task.relative { - let canon = ctx!(task, provider::canonicalize(task.to.parent().unwrap()).await)?; + let canon = ctx!(task, engine::canonicalize(task.to.parent().unwrap()).await)?; src = ctx!(task, path_relative_to(canon.loc(), src))?; } - ok_or_not_found!(task, provider::remove_file(&task.to).await); + ok_or_not_found!(task, engine::remove_file(&task.to).await); ctx!( task, - provider::symlink(&task.to, src, async || { + engine::symlink(&task.to, src, async || { Ok(match task.cha { Some(cha) => cha.is_dir(), None => Self::cha(&task.from, task.resolve, None).await?.is_dir(), @@ -225,7 +225,7 @@ impl File { )?; if task.delete { - provider::remove_file(&task.from).await.ok(); + engine::remove_file(&task.from).await.ok(); } Ok(self.ops.out(task.id, FileOutLink::Succ)) } @@ -241,7 +241,7 @@ impl File { self.ops.out(task.id, HookInOutHardlink::new(&task.from, &task.to)); super::traverse::( task, - async |dir| match provider::create_dir(dir).await { + async |dir| match engine::create_dir(dir).await { Err(e) if e.kind() != io::ErrorKind::AlreadyExists => Err(e)?, _ => Ok(()), }, @@ -261,14 +261,14 @@ impl File { pub(crate) async fn hardlink_do(&self, task: FileInHardlink) -> Result<(), FileOutHardlinkDo> { let src = if !task.follow { UrlCow::from(&task.from) - } else if let Ok(p) = provider::canonicalize(&task.from).await { + } else if let Ok(p) = engine::canonicalize(&task.from).await { UrlCow::from(p) } else { UrlCow::from(&task.from) }; - ok_or_not_found!(task, provider::remove_file(&task.to).await); - ok_or_not_found!(task, provider::hard_link(&src, &task.to).await); + ok_or_not_found!(task, engine::remove_file(&task.to).await); + ok_or_not_found!(task, engine::hard_link(&src, &task.to).await); Ok(self.ops.out(task.id, FileOutHardlinkDo::Succ)) } @@ -291,7 +291,7 @@ impl File { } pub(crate) async fn delete_do(&self, task: FileInDelete) -> Result<(), FileOutDeleteDo> { - match provider::remove_file(&task.target).await { + match engine::remove_file(&task.target).await { Ok(()) => {} Err(e) if e.kind() == NotFound => {} Err(_) if !maybe_exists(&task.target).await => {} @@ -305,7 +305,7 @@ impl File { } pub(crate) async fn trash_do(&self, task: FileInTrash) -> Result<(), FileOutTrash> { - ctx!(task, provider::trash(&task.target).await)?; + ctx!(task, engine::trash(&task.target).await)?; Ok(self.ops.out(task.id, FileOutTrash::Succ)) } @@ -345,12 +345,12 @@ impl File { let cache = ctx!(task, task.target.cache(), "Cannot determine cache path")?; let cache_tmp = ctx!(task, Transaction::tmp(&cache).await, "Cannot determine download cache")?; - let mut rx = ctx!(task, provider::copy_with_progress(&task.target, &cache_tmp, cha).await)?; + let mut rx = ctx!(task, engine::copy_progressive(&task.target, &cache_tmp, cha).await)?; loop { match rx.recv().await.unwrap_or(Ok(0)) { Ok(0) => { Local::regular(&cache).remove_dir_all().await.ok(); - ctx!(task, provider::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?; + ctx!(task, engine::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?; let lock = ctx!(task, task.target.cache_lock(), "Cannot determine cache lock")?; let hash = format!("{:x}", cha.hash_u128()); @@ -422,7 +422,7 @@ impl File { ctx!(task, Transaction::tmp(&task.target).await, "Cannot determine temporary upload path")?; let mut rx = ctx!( task, - provider::copy_with_progress(cache, &tmp, Attrs { + engine::copy_progressive(cache, &tmp, Attrs { mode: Some(cha.mode), atime: None, btime: None, @@ -440,7 +440,7 @@ impl File { Err(anyhow!("Failed to work on: {task:?}: remote file has changed during upload"))?; } - ctx!(task, provider::rename(&tmp, &task.target).await, "Cannot persist uploaded file")?; + ctx!(task, engine::rename(&tmp, &task.target).await, "Cannot persist uploaded file")?; let cha = ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat uploaded file")?; @@ -463,7 +463,7 @@ impl File { let cha = if let Some(entry) = entry { entry.metadata().await? } else { - provider::symlink_metadata(url.as_url()).await? + engine::symlink_metadata(url.as_url()).await? }; Ok(if follow { Cha::from_follow(url, cha).await } else { cha }) } diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 2a92ef16..17cd8e47 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -206,7 +206,7 @@ impl Drop for FileInCut { impl FileInCut { pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self { Self { - follow: !from.scheme().covariant(to.scheme()), + follow: !from.auth().covariant(to.auth()), id: Id::ZERO, from, to, diff --git a/yazi-scheduler/src/file/transaction.rs b/yazi-scheduler/src/file/transaction.rs index f812af7b..ddf7db60 100644 --- a/yazi-scheduler/src/file/transaction.rs +++ b/yazi-scheduler/src/file/transaction.rs @@ -1,9 +1,9 @@ use std::{hash::{BuildHasher, Hash, Hasher}, io}; -use yazi_fs::cha::ChaMode; +use yazi_fs::{cha::ChaMode, engine::Attrs}; use yazi_macro::ok_or_not_found; use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlBuf}}; -use yazi_vfs::{provider, unique_file}; +use yazi_vfs::{engine, unique_file}; pub(super) struct Transaction; @@ -33,11 +33,11 @@ impl Transaction { { let url = url.as_url(); - let cha = ok_or_not_found!(provider::symlink_metadata(url).await, return Ok(())); + let cha = ok_or_not_found!(engine::symlink_metadata(url).await, return Ok(())); if cha.is_link() { - provider::rename(Self::tmp(url).await?, url).await?; + engine::rename(Self::tmp(url).await?, url).await?; } else if !cha.contains(ChaMode::U_WRITE) { - provider::set_mode(url, cha.mode | ChaMode::U_WRITE).await?; + engine::set_attrs(url, Attrs::mode(cha.mode | ChaMode::U_WRITE)).await?; } Ok(()) diff --git a/yazi-scheduler/src/file/traverse.rs b/yazi-scheduler/src/file/traverse.rs index 4c47c7ad..866f9571 100644 --- a/yazi-scheduler/src/file/traverse.rs +++ b/yazi-scheduler/src/file/traverse.rs @@ -1,8 +1,8 @@ use std::{collections::VecDeque, fmt::Debug}; -use yazi_fs::{FsUrl, cha::Cha, path::skip_url, provider::{DirReader, FileHolder}}; +use yazi_fs::{FsUrl, cha::Cha, engine::{DirReader, FileHolder}, path::skip_url}; use yazi_shared::{strand::StrandLike, url::{AsUrl, Url, UrlBuf, UrlLike}}; -use yazi_vfs::provider::{self}; +use yazi_vfs::engine::{self}; use crate::{ctx, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInUpload}}; @@ -180,7 +180,7 @@ where } while let Some(src) = dirs.pop_front() { - let mut it = err!(provider::read_dir(&src).await, "Cannot read directory {src:?}"); + let mut it = err!(engine::read_dir(&src).await, "Cannot read directory {src:?}"); let dest = if let Some(root) = root { let s = skip_url(&src, skip); diff --git a/yazi-scheduler/src/hook/hook.rs b/yazi-scheduler/src/hook/hook.rs index b4a23837..3f82a4c6 100644 --- a/yazi-scheduler/src/hook/hook.rs +++ b/yazi-scheduler/src/hook/hook.rs @@ -4,7 +4,7 @@ use parking_lot::Mutex; use tokio::sync::mpsc; use yazi_dds::Pump; use yazi_fs::ok_or_not_found; -use yazi_vfs::provider; +use yazi_vfs::engine; use crate::{Ongoing, TaskOp, TaskOps, TasksProxy, file::{FileOutCopy, FileOutCut, FileOutDelete, FileOutDownload, FileOutHardlink, FileOutLink, FileOutTrash, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink, HookInPreload, HookInTrash, HookInUpload}, preload::{Preload, PreloadOut}}; @@ -31,7 +31,7 @@ impl Hook { return self.ops.out(task.id, FileOutCut::Clean(Ok(()))); } - let result = ok_or_not_found(provider::remove_dir_clean(&task.from).await); + let result = ok_or_not_found(engine::remove_dir_clean(&task.from).await); TasksProxy::update_succeed(task.id, [&task.to, &task.from], true); Pump::push_move(task.from, task.to); @@ -52,7 +52,7 @@ impl Hook { return self.ops.out(task.id, FileOutDelete::Clean(Ok(()))); } - let result = ok_or_not_found(provider::remove_dir_all(&task.target).await); + let result = ok_or_not_found(engine::remove_dir_all(&task.target).await); TasksProxy::update_succeed(task.id, [&task.target], false); Pump::push_delete(task.target); diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index b8dae4c3..67e5ac9b 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -78,7 +78,7 @@ impl Scheduler { } pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { - let follow = follow || !from.scheme().covariant(to.scheme()); + let follow = follow || !from.auth().covariant(to.auth()); let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 }; self.add(&mut r#in, |_| ()); @@ -109,7 +109,7 @@ impl Scheduler { let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow }; self.add(&mut r#in, |_| ()); - if !r#in.from.scheme().covariant(r#in.to.scheme()) { + if !r#in.from.auth().covariant(r#in.to.auth()) { return self .ops .out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned())); diff --git a/yazi-scheduler/src/size/size.rs b/yazi-scheduler/src/size/size.rs index bed30a91..bcd01646 100644 --- a/yazi-scheduler/src/size/size.rs +++ b/yazi-scheduler/src/size/size.rs @@ -4,7 +4,7 @@ use parking_lot::RwLock; use tokio::sync::mpsc; use yazi_fs::FilesOp; use yazi_shared::url::{UrlBuf, UrlLike}; -use yazi_vfs::provider; +use yazi_vfs::engine; use super::SizeIn; use crate::{TaskOp, TaskOps, size::SizeOut}; @@ -25,7 +25,7 @@ impl Size { } pub(crate) async fn size(&self, task: SizeIn) -> Result<(), SizeOut> { - let length = provider::calculate(&task.target).await.unwrap_or(0); + let length = engine::calculate(&task.target).await.unwrap_or(0); task.throttle.done((task.target, length), |buf| { { let mut loading = self.sizing.write(); diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs new file mode 100644 index 00000000..3a6efe2f --- /dev/null +++ b/yazi-shared/src/auth/auth.rs @@ -0,0 +1,65 @@ +use std::{fmt, sync::Arc}; + +use anyhow::{Result, anyhow}; +use percent_encoding::percent_decode_str; +use serde::Deserialize; +use yazi_shim::{SStr, cell::RoCell}; + +use crate::auth::{AuthInventory, AuthKind, Encode, Scheme}; + +pub(super) static DEFAULT_ARC: RoCell> = RoCell::new(); + +#[derive(Debug, Deserialize, Eq, Hash, PartialEq)] +pub struct Auth { + #[serde(default)] + pub kind: AuthKind, + pub scheme: Scheme, + #[serde(default)] + pub domain: SStr, +} + +impl Default for Auth { + fn default() -> Self { Self::DEFAULT } +} + +impl fmt::Display for Auth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Encode(self, false).fmt(f) } +} + +impl Auth { + pub const DEFAULT: Self = + Self { kind: AuthKind::Regular, scheme: Scheme::Regular, domain: SStr::Borrowed("") }; + + pub fn default_arc() -> Arc { DEFAULT_ARC.clone() } + + pub fn new(kind: AuthKind, scheme: Scheme, domain: impl Into) -> Arc { + Arc::new(Self { kind, scheme, domain: domain.into() }) + } + + pub fn get(scheme: &Scheme, domain: &str) -> Option> { + match scheme { + Scheme::Regular => Some(Self::default_arc()), + Scheme::Search => Some(Self::search(domain)), + _ => inventory::iter::().find_map(|entry| (entry.get)(scheme, domain)), + } + } + + pub fn search(query: impl Into) -> Arc { + Self::new(AuthKind::Search, Scheme::Search, query.into()) + } + + pub fn covariant(&self, other: &Self) -> bool { + !self.kind.is_virtual() && !other.kind.is_virtual() || self == other + } + + pub fn parse_cache(cache: &str) -> Result> { + let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; + let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; + + Ok(Arc::new(Self { + kind: kind.parse()?, + scheme: scheme.parse()?, + domain: percent_decode_str(domain).decode_utf8()?.into_owned().into(), + })) + } +} diff --git a/yazi-shared/src/auth/encode.rs b/yazi-shared/src/auth/encode.rs new file mode 100644 index 00000000..043da080 --- /dev/null +++ b/yazi-shared/src/auth/encode.rs @@ -0,0 +1,26 @@ +use std::fmt; + +use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; + +use super::Auth; + +pub struct Encode<'a>(pub &'a Auth, pub bool); + +impl Encode<'_> { + pub fn domain(s: &str) -> PercentEncode<'_> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); + percent_encode(s.as_bytes(), SET) + } +} + +impl fmt::Display for Encode<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}://{}", + self.0.scheme, + if self.1 { "~" } else { "" }, + Self::domain(&self.0.domain) + ) + } +} diff --git a/yazi-shared/src/auth/inventory.rs b/yazi-shared/src/auth/inventory.rs new file mode 100644 index 00000000..a722d68d --- /dev/null +++ b/yazi-shared/src/auth/inventory.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +use crate::auth::{Auth, Scheme}; + +pub struct AuthInventory { + pub get: fn(&Scheme, &str) -> Option>, +} + +inventory::collect!(AuthInventory); diff --git a/yazi-shared/src/auth/kind.rs b/yazi-shared/src/auth/kind.rs new file mode 100644 index 00000000..ed794c77 --- /dev/null +++ b/yazi-shared/src/auth/kind.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; +use strum::{EnumString, IntoStaticStr}; + +#[derive( + Clone, Copy, Debug, Default, Deserialize, EnumString, Eq, Hash, IntoStaticStr, PartialEq, +)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum AuthKind { + #[default] + Regular, + Search, + Mount, + Scope, + Sftp, +} + +impl AuthKind { + #[inline] + pub fn is_local(self) -> bool { + match self { + Self::Regular | Self::Search => true, + Self::Mount | Self::Scope | Self::Sftp => false, + } + } + + #[inline] + pub fn is_remote(self) -> bool { + match self { + Self::Regular | Self::Search | Self::Mount => false, + Self::Scope | Self::Sftp => true, + } + } + + #[inline] + pub fn is_virtual(self) -> bool { + match self { + Self::Regular | Self::Search => false, + Self::Mount | Self::Scope | Self::Sftp => true, + } + } +} diff --git a/yazi-shared/src/auth/mod.rs b/yazi-shared/src/auth/mod.rs new file mode 100644 index 00000000..55f581e7 --- /dev/null +++ b/yazi-shared/src/auth/mod.rs @@ -0,0 +1,3 @@ +yazi_macro::mod_flat!(auth encode inventory kind scheme); + +pub(super) fn init() { DEFAULT_ARC.with(Default::default); } diff --git a/yazi-shared/src/auth/scheme.rs b/yazi-shared/src/auth/scheme.rs new file mode 100644 index 00000000..2e4fd3c3 --- /dev/null +++ b/yazi-shared/src/auth/scheme.rs @@ -0,0 +1,63 @@ +use std::{fmt, str::FromStr}; + +use anyhow::{Result, bail}; +use serde_with::DeserializeFromStr; + +use crate::{BytesExt, pool::{InternStr, Symbol}}; + +#[derive(Clone, Debug, DeserializeFromStr, Eq, Hash, PartialEq)] +pub enum Scheme { + Regular, + Search, + Sftp, + Custom(Symbol), +} + +impl AsRef for Scheme { + fn as_ref(&self) -> &str { self.as_str() } +} + +impl PartialEq for Scheme { + fn eq(&self, other: &str) -> bool { self.as_str() == other } +} + +impl PartialEq<&str> for Scheme { + fn eq(&self, other: &&str) -> bool { self == *other } +} + +impl PartialEq<&String> for Scheme { + fn eq(&self, other: &&String) -> bool { self == other.as_str() } +} + +impl PartialEq<&Self> for Scheme { + fn eq(&self, other: &&Self) -> bool { self == *other } +} + +impl fmt::Display for Scheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } +} + +impl FromStr for Scheme { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "regular" => Self::Regular, + "search" => Self::Search, + "sftp" => Self::Sftp, + _ if !s.is_empty() && s.as_bytes().kebab_cased() => Self::Custom(s.intern()), + _ => bail!("scheme must be kebab-case and non-empty, got: {s}"), + }) + } +} + +impl Scheme { + pub fn as_str(&self) -> &str { + match self { + Self::Regular => "regular", + Self::Search => "search", + Self::Sftp => "sftp", + Self::Custom(s) => s, + } + } +} diff --git a/yazi-shared/src/kebab_cased_string.rs b/yazi-shared/src/kebab_cased_key.rs similarity index 57% rename from yazi-shared/src/kebab_cased_string.rs rename to yazi-shared/src/kebab_cased_key.rs index 01ef687b..99d5b496 100644 --- a/yazi-shared/src/kebab_cased_string.rs +++ b/yazi-shared/src/kebab_cased_key.rs @@ -6,10 +6,12 @@ use crate::{BytesExt, SnakeCasedString}; #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] #[serde(transparent)] -pub struct KebabCasedString(String); +pub struct KebabCasedKey(String); -impl KebabCasedString { - pub fn new(s: String) -> Option { s.as_bytes().kebab_cased().then_some(Self(s)) } +impl KebabCasedKey { + pub fn new(s: String) -> Option { + (!s.is_empty() && s.len() < 20 && s.as_bytes().kebab_cased()).then_some(Self(s)) + } pub fn into_snake_cased(self) -> SnakeCasedString { let mut b = self.0.into_bytes(); @@ -22,50 +24,52 @@ impl KebabCasedString { } } -impl Deref for KebabCasedString { +impl Deref for KebabCasedKey { type Target = str; #[inline] fn deref(&self) -> &Self::Target { &self.0 } } -impl Borrow for KebabCasedString { +impl Borrow for KebabCasedKey { #[inline] fn borrow(&self) -> &str { &self.0 } } -impl Borrow for KebabCasedString { +impl Borrow for KebabCasedKey { #[inline] fn borrow(&self) -> &String { &self.0 } } -impl AsRef for KebabCasedString { +impl AsRef for KebabCasedKey { #[inline] fn as_ref(&self) -> &str { &self.0 } } -impl AsRef for KebabCasedString { +impl AsRef for KebabCasedKey { #[inline] fn as_ref(&self) -> &OsStr { self.0.as_ref() } } -impl Display for KebabCasedString { +impl Display for KebabCasedKey { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.0, f) } } -impl From for String { +impl From for String { #[inline] - fn from(value: KebabCasedString) -> Self { value.0 } + fn from(value: KebabCasedKey) -> Self { value.0 } } -impl From for Cow<'_, str> { +impl From for Cow<'_, str> { #[inline] - fn from(value: KebabCasedString) -> Self { Cow::Owned(value.0) } + fn from(value: KebabCasedKey) -> Self { Cow::Owned(value.0) } } -impl<'de> Deserialize<'de> for KebabCasedString { +impl<'de> Deserialize<'de> for KebabCasedKey { fn deserialize>(deserializer: D) -> Result { let value = String::deserialize(deserializer)?; - Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a kebab-cased string")) + Self::new(value).ok_or_else(|| { + serde::de::Error::custom("must be a non-empty kebab-cased key shorter than 20 characters") + }) } } diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index c9e663d8..452728a9 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,8 +1,8 @@ extern crate self as yazi_shared; -yazi_macro::mod_pub!(any_data data event id loc path pool scheme shell strand translit url); +yazi_macro::mod_pub!(any_data auth data spec event id loc path pool shell strand translit url); -yazi_macro::mod_flat!(bytes chars completion_token condition debounce env kebab_cased_string last_value layer localset natsort non_empty_string os predictor snake_cased_string source tests throttle time); +yazi_macro::mod_flat!(bytes chars completion_token condition debounce env kebab_cased_key last_value layer localset natsort non_empty_string os predictor snake_cased_string source tests throttle time); pub fn init() { LOCAL_SET.with(tokio::task::LocalSet::new); @@ -13,5 +13,6 @@ pub fn init() { USERS_CACHE.with(<_>::default); pool::init(); + auth::init(); event::Event::init(); } diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 52d1cfbf..f00c77eb 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -2,7 +2,7 @@ use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, use anyhow::Result; -use crate::{loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, scheme::SchemeKind, strand::AsStrandView}; +use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, strand::AsStrandView}; #[derive(Clone, Default, Eq, PartialEq)] pub struct LocBuf

{ @@ -146,7 +146,7 @@ where Self { inner: loc.inner, uri, urn } } - pub fn saturated(path: P, kind: SchemeKind) -> Self { + pub fn saturated(path: P, kind: AuthKind) -> Self { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::saturated(&loc.inner, kind); @@ -332,18 +332,18 @@ mod tests { // Regular ("/", "a", "/a"), ("/a/b", "c", "/a/c"), - // Archive - ("archive:////", "a.zip", "archive:////a.zip"), - ("archive:////a.zip/b", "c", "archive:////a.zip/c"), - ("archive://:2//a.zip/b", "c", "archive://:2//a.zip/c"), - ("archive://:2:1//a.zip/b", "c", "archive://:2:1//a.zip/c"), + // Mount + ("test-mount://7z//", "a.zip", "test-mount://7z//a.zip"), + ("test-mount://7z//a.zip/b", "c", "test-mount://7z//a.zip/c"), + ("test-mount://7z:2//a.zip/b", "c", "test-mount://7z:2//a.zip/c"), + ("test-mount://7z:2:1//a.zip/b", "c", "test-mount://7z:2:1//a.zip/c"), // Empty ("/a", "", "/"), - ("archive:////a.zip", "", "archive:////"), - ("archive:////a.zip/b", "", "archive:////a.zip"), - ("archive://:1:1//a.zip", "", "archive:////"), - ("archive://:2//a.zip/b", "", "archive://:1//a.zip"), - ("archive://:2:2//a.zip/b", "", "archive://:1:1//a.zip"), + ("test-mount://7z//a.zip", "", "test-mount://7z//"), + ("test-mount://7z//a.zip/b", "", "test-mount://7z//a.zip"), + ("test-mount://7z:1:1//a.zip", "", "test-mount://7z//"), + ("test-mount://7z:2//a.zip/b", "", "test-mount://7z:1//a.zip"), + ("test-mount://7z:2:2//a.zip/b", "", "test-mount://7z:1:1//a.zip"), ]; for (input, name, expected) in cases { diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index a1824ee8..533b7c2a 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -3,7 +3,7 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref}; use anyhow::{Result, bail}; use super::LocAbleImpl; -use crate::{loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, scheme::SchemeKind, strand::AsStrandView}; +use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, strand::AsStrandView}; #[derive(Clone, Copy, Debug)] pub struct Loc<'p, P = &'p std::path::Path> { @@ -147,15 +147,16 @@ where self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty()) } - pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self + pub fn saturated<'a, T>(path: T, kind: AuthKind) -> Self where T: AsPathView<'p, P>, { match kind { - SchemeKind::Regular => Self::bare(path), - SchemeKind::Search => Self::zeroed(path), - SchemeKind::Archive => Self::zeroed(path), - SchemeKind::Sftp => Self::bare(path), + AuthKind::Regular => Self::bare(path), + AuthKind::Search => Self::zeroed(path), + AuthKind::Mount => Self::zeroed(path), + AuthKind::Scope => Self::bare(path), + AuthKind::Sftp => Self::bare(path), } } diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index fb2c9a1f..68b59865 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::scheme::SchemeKind; +use crate::auth::AuthKind; #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] @@ -9,13 +9,14 @@ pub enum PathKind { Unix, } -impl From for PathKind { - fn from(value: SchemeKind) -> Self { +impl From for PathKind { + fn from(value: AuthKind) -> Self { match value { - SchemeKind::Regular => Self::Os, - SchemeKind::Search => Self::Os, - SchemeKind::Archive => Self::Os, - SchemeKind::Sftp => Self::Unix, + AuthKind::Regular => Self::Os, + AuthKind::Search => Self::Os, + AuthKind::Mount => Self::Os, + AuthKind::Scope => Self::Unix, + AuthKind::Sftp => Self::Unix, } } } diff --git a/yazi-shared/src/pool/mod.rs b/yazi-shared/src/pool/mod.rs index 4aa99859..396f37f2 100644 --- a/yazi-shared/src/pool/mod.rs +++ b/yazi-shared/src/pool/mod.rs @@ -4,7 +4,7 @@ static SYMBOLS: yazi_shim::cell::RoCell< parking_lot::Mutex>, > = yazi_shim::cell::RoCell::new(); -pub(super) fn init() { SYMBOLS.with(<_>::default); } +pub(super) fn init() { SYMBOLS.with(Default::default); } #[inline] pub(super) fn compute_hash(value: T) -> u64 { diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs deleted file mode 100644 index 30a4eea0..00000000 --- a/yazi-shared/src/scheme/cow.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{Result, ensure}; -use percent_encoding::percent_decode; - -use crate::{path::{PathCow, PathLike}, pool::{InternStr, SymbolCow}, scheme::{AsScheme, Scheme, SchemeKind, SchemeRef}, url::Url}; - -#[derive(Clone, Debug)] -pub enum SchemeCow<'a> { - Borrowed(SchemeRef<'a>), - Owned(Scheme), -} - -impl<'a> From> for SchemeCow<'a> { - fn from(value: SchemeRef<'a>) -> Self { Self::Borrowed(value) } -} - -impl<'a, T> From<&'a T> for SchemeCow<'a> -where - T: AsScheme + ?Sized, -{ - fn from(value: &'a T) -> Self { Self::Borrowed(value.as_scheme()) } -} - -impl From for SchemeCow<'_> { - fn from(value: Scheme) -> Self { Self::Owned(value) } -} - -impl From> for Scheme { - fn from(value: SchemeCow<'_>) -> Self { value.into_owned() } -} - -impl PartialEq> for SchemeCow<'_> { - fn eq(&self, other: &SchemeRef) -> bool { self.as_scheme() == *other } -} - -impl<'a> SchemeCow<'a> { - pub fn regular(uri: usize, urn: usize) -> Self { SchemeRef::Regular { uri, urn }.into() } - - pub fn search(domain: T, uri: usize, urn: usize) -> Self - where - T: Into>, - { - match domain.into() { - Cow::Borrowed(domain) => SchemeRef::Search { domain, uri, urn }.into(), - Cow::Owned(domain) => Scheme::Search { domain: domain.intern(), uri, urn }.into(), - } - } - - pub fn archive(domain: T, uri: usize, urn: usize) -> Self - where - T: Into>, - { - match domain.into() { - Cow::Borrowed(domain) => SchemeRef::Archive { domain, uri, urn }.into(), - Cow::Owned(domain) => Scheme::Archive { domain: domain.intern(), uri, urn }.into(), - } - } - - pub fn sftp(domain: T, uri: usize, urn: usize) -> Self - where - T: Into>, - { - match domain.into() { - Cow::Borrowed(domain) => SchemeRef::Sftp { domain, uri, urn }.into(), - Cow::Owned(domain) => Scheme::Sftp { domain: domain.intern(), uri, urn }.into(), - } - } - - pub fn parse(bytes: &'a [u8]) -> Result<(Self, PathCow<'a>)> { - let Some((kind, tilde)) = SchemeKind::parse(bytes)? else { - let path = Self::decode_path(SchemeKind::Regular, false, bytes)?; - let (uri, urn) = Self::normalize_ports(SchemeKind::Regular, None, None, &path)?; - return Ok((Self::regular(uri, urn), path)); - }; - - // Decode domain and ports - let mut skip = kind.offset(tilde); - let (domain, uri, urn) = match kind { - SchemeKind::Regular => ("".into(), None, None), - SchemeKind::Search => Self::decode_param(&bytes[skip..], &mut skip)?, - SchemeKind::Archive => Self::decode_param(&bytes[skip..], &mut skip)?, - SchemeKind::Sftp => Self::decode_param(&bytes[skip..], &mut skip)?, - }; - - // Decode path - let path = Self::decode_path(kind, tilde, &bytes[skip..])?; - - // Build scheme - let (uri, urn) = Self::normalize_ports(kind, uri, urn, &path)?; - let scheme = match kind { - SchemeKind::Regular => Self::regular(uri, urn), - SchemeKind::Search => Self::search(domain, uri, urn), - SchemeKind::Archive => Self::archive(domain, uri, urn), - SchemeKind::Sftp => Self::sftp(domain, uri, urn), - }; - - Ok((scheme, path)) - } - - fn decode_param( - bytes: &'a [u8], - skip: &mut usize, - ) -> Result<(Cow<'a, str>, Option, Option)> { - let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); - let slash = bytes.get(len).is_some_and(|&b| b == b'/'); - *skip += len + slash as usize; - - let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; - let domain = match Cow::from(percent_decode(&bytes[..len])) { - Cow::Borrowed(b) => str::from_utf8(b)?.into(), - Cow::Owned(b) => String::from_utf8(b)?.into(), - }; - - Ok((domain, uri, urn)) - } - - fn decode_ports(bytes: &[u8], skip: &mut usize) -> Result<(Option, Option)> { - let Some(a_idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok((None, None)) }; - let a_len = bytes.len() - a_idx; - *skip -= a_len; - let a = if a_len == 1 { None } else { Some(str::from_utf8(&bytes[a_idx + 1..])?.parse()?) }; - - let Some(b_idx) = bytes[..a_idx].iter().rposition(|&b| b == b':') else { - return Ok((a, None)); - }; - let b_len = bytes[..a_idx].len() - b_idx; - *skip -= b_len; - let b = - if b_len == 1 { None } else { Some(str::from_utf8(&bytes[b_idx + 1..a_idx])?.parse()?) }; - - Ok((b, a)) - } - - fn decode_path(kind: SchemeKind, tilde: bool, bytes: &'a [u8]) -> Result> { - let bytes: Cow<_> = if tilde { percent_decode(bytes).into() } else { bytes.into() }; - PathCow::with(kind, bytes) - } - - fn normalize_ports( - kind: SchemeKind, - uri: Option, - urn: Option, - path: &PathCow, - ) -> Result<(usize, usize)> { - Ok(match kind { - SchemeKind::Regular => { - ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); - (path.name().is_some() as usize, path.name().is_some() as usize) - } - SchemeKind::Search => { - let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); - ensure!(uri == urn, "Search scheme requires URI and URN to be equal"); - (uri, urn) - } - SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)), - SchemeKind::Sftp => { - let uri = uri.unwrap_or(path.name().is_some() as usize); - let urn = urn.unwrap_or(path.name().is_some() as usize); - (uri, urn) - } - }) - } - - pub fn retrieve_ports(url: Url) -> (usize, usize) { - match url { - Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), - Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), - Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), - Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), - } - } -} - -impl<'a> SchemeCow<'a> { - #[inline] - pub fn into_domain(self) -> Option> { - Some(match self { - SchemeCow::Borrowed(s) => s.domain()?.into(), - SchemeCow::Owned(s) => s.into_domain()?.into(), - }) - } - - #[inline] - pub fn into_owned(self) -> Scheme { - match self { - Self::Borrowed(s) => s.to_owned(), - Self::Owned(s) => s, - } - } - - pub fn with_ports(self, uri: usize, urn: usize) -> Self { - match self { - Self::Borrowed(s) => s.with_ports(uri, urn).into(), - Self::Owned(s) => s.with_ports(uri, urn).into(), - } - } - - #[inline] - pub fn zeroed(self) -> Self { self.with_ports(0, 0) } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_decode_ports() -> Result<()> { - fn assert(s: &str, len: usize, uri: Option, urn: Option) -> Result<()> { - let mut n = usize::MAX; - let port = SchemeCow::decode_ports(s.as_bytes(), &mut n)?; - assert_eq!((usize::MAX - n, port.0, port.1), (len, uri, urn)); - Ok(()) - } - - // Zeros - assert("", 0, None, None)?; - assert(":", 1, None, None)?; - assert("::", 2, None, None)?; - - // URI - assert(":2", 2, Some(2), None)?; - assert(":2:", 3, Some(2), None)?; - assert(":22:", 4, Some(22), None)?; - - // URN - assert("::1", 3, None, Some(1))?; - assert(":2:1", 4, Some(2), Some(1))?; - assert(":22:11", 6, Some(22), Some(11))?; - Ok(()) - } -} diff --git a/yazi-shared/src/scheme/inventory.rs b/yazi-shared/src/scheme/inventory.rs deleted file mode 100644 index dcc850d0..00000000 --- a/yazi-shared/src/scheme/inventory.rs +++ /dev/null @@ -1,9 +0,0 @@ -use mlua::UserDataRegistry; - -use crate::scheme::Scheme; - -pub struct SchemeInventory { - pub register: fn(&mut UserDataRegistry), -} - -inventory::collect!(SchemeInventory); diff --git a/yazi-shared/src/scheme/kind.rs b/yazi-shared/src/scheme/kind.rs deleted file mode 100644 index c5ddc3dc..00000000 --- a/yazi-shared/src/scheme/kind.rs +++ /dev/null @@ -1,84 +0,0 @@ -use anyhow::{Result, bail}; -use strum::IntoStaticStr; - -use crate::{BytesExt, scheme::{AsScheme, SchemeRef}}; - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum SchemeKind { - Regular, - Search, - Archive, - Sftp, -} - -impl From for SchemeKind -where - T: AsScheme, -{ - fn from(value: T) -> Self { - match value.as_scheme() { - SchemeRef::Regular { .. } => Self::Regular, - SchemeRef::Search { .. } => Self::Search, - SchemeRef::Archive { .. } => Self::Archive, - SchemeRef::Sftp { .. } => Self::Sftp, - } - } -} - -impl TryFrom<&[u8]> for SchemeKind { - type Error = anyhow::Error; - - fn try_from(value: &[u8]) -> Result { - match value { - b"regular" => Ok(Self::Regular), - b"search" => Ok(Self::Search), - b"archive" => Ok(Self::Archive), - b"sftp" => Ok(Self::Sftp), - _ => bail!("invalid scheme kind: {}", String::from_utf8_lossy(value)), - } - } -} - -impl SchemeKind { - #[inline] - pub fn is_local(self) -> bool { - match self { - Self::Regular | Self::Search => true, - Self::Archive | Self::Sftp => false, - } - } - - #[inline] - pub fn is_remote(self) -> bool { - match self { - Self::Regular | Self::Search | Self::Archive => false, - Self::Sftp => true, - } - } - - #[inline] - pub fn is_virtual(self) -> bool { - match self { - Self::Regular | Self::Search => false, - Self::Archive | Self::Sftp => true, - } - } - - #[inline] - pub(super) fn offset(self, tilde: bool) -> usize { - 3 + Into::<&str>::into(self).len() + tilde as usize - } - - pub fn parse(bytes: &[u8]) -> Result> { - let Some((kind, _)) = bytes.split_seq_once(b"://") else { - return Ok(None); - }; - - Ok(Some(if let Some(stripped) = kind.strip_suffix(b"~") { - (Self::try_from(stripped)?, true) - } else { - (Self::try_from(kind)?, false) - })) - } -} diff --git a/yazi-shared/src/scheme/lua.rs b/yazi-shared/src/scheme/lua.rs deleted file mode 100644 index 5764a70b..00000000 --- a/yazi-shared/src/scheme/lua.rs +++ /dev/null @@ -1,20 +0,0 @@ -use mlua::{UserData, UserDataFields, UserDataRegistry}; -use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; - -use crate::scheme::{Scheme, SchemeInventory, SchemeLike}; - -impl UserData for Scheme { - fn add_fields>(fields: &mut F) { - fields.add_cached_field("kind", |_, me| Ok(me.kind().into_str())); - fields.add_field_method_get("is_virtual", |_, me| Ok(me.is_virtual())); - } - - fn register(registry: &mut UserDataRegistry) { - Self::add_fields(registry); - Self::add_methods(registry); - - for inv in inventory::iter::() { - (inv.register)(registry); - } - } -} diff --git a/yazi-shared/src/scheme/mod.rs b/yazi-shared/src/scheme/mod.rs deleted file mode 100644 index 465e626b..00000000 --- a/yazi-shared/src/scheme/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(cow encode inventory kind lua r#ref scheme traits); diff --git a/yazi-shared/src/scheme/ref.rs b/yazi-shared/src/scheme/ref.rs deleted file mode 100644 index 6259f699..00000000 --- a/yazi-shared/src/scheme/ref.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{hash::Hash, ops::Deref}; - -use crate::{pool::InternStr, scheme::{AsScheme, Scheme, SchemeKind}}; - -#[derive(Clone, Copy, Debug)] -pub enum SchemeRef<'a> { - Regular { uri: usize, urn: usize }, - Search { domain: &'a str, uri: usize, urn: usize }, - Archive { domain: &'a str, uri: usize, urn: usize }, - Sftp { domain: &'a str, uri: usize, urn: usize }, -} - -impl Deref for SchemeRef<'_> { - type Target = SchemeKind; - - fn deref(&self) -> &Self::Target { - match self { - Self::Regular { .. } => &SchemeKind::Regular, - Self::Search { .. } => &SchemeKind::Search, - Self::Archive { .. } => &SchemeKind::Archive, - Self::Sftp { .. } => &SchemeKind::Sftp, - } - } -} - -impl Hash for SchemeRef<'_> { - fn hash(&self, state: &mut H) { - self.kind().hash(state); - self.domain().hash(state); - } -} - -impl PartialEq> for SchemeRef<'_> { - fn eq(&self, other: &SchemeRef) -> bool { - self.kind() == other.kind() && self.domain() == other.domain() - } -} - -impl From> for Scheme { - fn from(value: SchemeRef) -> Self { value.to_owned() } -} - -impl<'a> SchemeRef<'a> { - pub fn covariant(self, other: impl AsScheme) -> bool { - let other = other.as_scheme(); - if self.is_virtual() || other.is_virtual() { self == other } else { true } - } - - pub const fn domain(self) -> Option<&'a str> { - match self { - Self::Regular { .. } => None, - Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => { - Some(domain) - } - } - } - - pub const fn kind(self) -> SchemeKind { - match self { - Self::Regular { .. } => SchemeKind::Regular, - Self::Search { .. } => SchemeKind::Search, - Self::Archive { .. } => SchemeKind::Archive, - Self::Sftp { .. } => SchemeKind::Sftp, - } - } - - pub const fn ports(self) -> (usize, usize) { - match self { - Self::Regular { uri, urn } => (uri, urn), - Self::Search { uri, urn, .. } => (uri, urn), - Self::Archive { uri, urn, .. } => (uri, urn), - Self::Sftp { uri, urn, .. } => (uri, urn), - } - } - - pub fn to_owned(self) -> Scheme { - match self { - Self::Regular { uri, urn } => Scheme::Regular { uri, urn }, - Self::Search { domain, uri, urn } => Scheme::Search { domain: domain.intern(), uri, urn }, - Self::Archive { domain, uri, urn } => Scheme::Archive { domain: domain.intern(), uri, urn }, - Self::Sftp { domain, uri, urn } => Scheme::Sftp { domain: domain.intern(), uri, urn }, - } - } - - pub const fn with_ports(self, uri: usize, urn: usize) -> Self { - match self { - Self::Regular { .. } => Self::Regular { uri, urn }, - Self::Search { domain, .. } => Self::Search { domain, uri, urn }, - Self::Archive { domain, .. } => Self::Archive { domain, uri, urn }, - Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn }, - } - } - - pub const fn zeroed(self) -> Self { self.with_ports(0, 0) } -} diff --git a/yazi-shared/src/scheme/scheme.rs b/yazi-shared/src/scheme/scheme.rs deleted file mode 100644 index 8148a431..00000000 --- a/yazi-shared/src/scheme/scheme.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::hash::{Hash, Hasher}; - -use serde::Deserialize; - -use crate::{pool::Symbol, scheme::{AsScheme, SchemeRef}}; - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(tag = "kind", rename_all = "kebab-case")] -pub enum Scheme { - Regular { uri: usize, urn: usize }, - Search { domain: Symbol, uri: usize, urn: usize }, - Archive { domain: Symbol, uri: usize, urn: usize }, - Sftp { domain: Symbol, uri: usize, urn: usize }, -} - -impl Hash for Scheme { - fn hash(&self, state: &mut H) { self.as_scheme().hash(state); } -} - -impl PartialEq> for Scheme { - fn eq(&self, other: &SchemeRef<'_>) -> bool { self.as_scheme() == *other } -} - -impl Scheme { - #[inline] - pub fn into_domain(self) -> Option> { - match self { - Self::Regular { .. } => None, - Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => { - Some(domain) - } - } - } - - #[inline] - pub fn with_ports(self, uri: usize, urn: usize) -> Self { - match self { - Self::Regular { .. } => Self::Regular { uri, urn }, - Self::Search { domain, .. } => Self::Search { domain, uri, urn }, - Self::Archive { domain, .. } => Self::Archive { domain, uri, urn }, - Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn }, - } - } - - #[inline] - pub fn zeroed(self) -> Self { self.with_ports(0, 0) } -} diff --git a/yazi-shared/src/scheme/traits.rs b/yazi-shared/src/scheme/traits.rs deleted file mode 100644 index b36cbc56..00000000 --- a/yazi-shared/src/scheme/traits.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::scheme::{Scheme, SchemeCow, SchemeKind, SchemeRef}; - -pub trait AsScheme { - fn as_scheme(&self) -> SchemeRef<'_>; -} - -impl AsScheme for SchemeRef<'_> { - #[inline] - fn as_scheme(&self) -> SchemeRef<'_> { *self } -} - -impl AsScheme for Scheme { - #[inline] - fn as_scheme(&self) -> SchemeRef<'_> { - match *self { - Self::Regular { uri, urn } => SchemeRef::Regular { uri, urn }, - Self::Search { ref domain, uri, urn } => SchemeRef::Search { domain, uri, urn }, - Self::Archive { ref domain, uri, urn } => SchemeRef::Archive { domain, uri, urn }, - Self::Sftp { ref domain, uri, urn } => SchemeRef::Sftp { domain, uri, urn }, - } - } -} - -impl AsScheme for &Scheme { - #[inline] - fn as_scheme(&self) -> SchemeRef<'_> { (**self).as_scheme() } -} - -impl AsScheme for SchemeCow<'_> { - #[inline] - fn as_scheme(&self) -> SchemeRef<'_> { - match self { - SchemeCow::Borrowed(s) => *s, - SchemeCow::Owned(s) => s.as_scheme(), - } - } -} - -impl AsScheme for &SchemeCow<'_> { - #[inline] - fn as_scheme(&self) -> SchemeRef<'_> { (**self).as_scheme() } -} - -// --- SchemeLike -pub trait SchemeLike -where - Self: AsScheme + Sized, -{ - fn kind(&self) -> SchemeKind { *self.as_scheme() } - - fn ports(&self) -> (usize, usize) { self.as_scheme().ports() } - - fn domain(&self) -> Option<&str> { self.as_scheme().domain() } - - fn covariant(&self, other: impl AsScheme) -> bool { self.as_scheme().covariant(other) } - - fn is_local(&self) -> bool { self.as_scheme().is_local() } - - fn is_remote(&self) -> bool { self.as_scheme().is_remote() } - - fn is_virtual(&self) -> bool { self.as_scheme().is_virtual() } -} - -impl SchemeLike for Scheme {} -impl SchemeLike for SchemeCow<'_> {} diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/spec/encode.rs similarity index 58% rename from yazi-shared/src/scheme/encode.rs rename to yazi-shared/src/spec/encode.rs index c51e28ea..79728da1 100644 --- a/yazi-shared/src/scheme/encode.rs +++ b/yazi-shared/src/spec/encode.rs @@ -1,8 +1,6 @@ use std::fmt::{self, Display}; -use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; - -use crate::{scheme::SchemeKind, url::Url}; +use crate::{auth::{AuthKind, Encode as EncodeAuth}, url::Url}; #[derive(Clone, Copy)] pub struct Encode<'a>(pub Url<'a>); @@ -12,12 +10,6 @@ impl<'a> From> for Encode<'a> { } impl<'a> Encode<'a> { - #[inline] - pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> { - const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); - percent_encode(s.as_bytes(), SET) - } - pub(crate) fn ports(self) -> impl Display { struct D<'a>(Encode<'a>); @@ -25,7 +17,7 @@ impl<'a> Encode<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { macro_rules! w { ($default_uri:expr, $default_urn:expr) => {{ - let (uri, urn) = self.0.0.scheme().ports(); + let (uri, urn) = self.0.0.spec().ports(); match (uri != $default_uri, urn != $default_urn) { (true, true) => write!(f, ":{uri}:{urn}"), (true, false) => write!(f, ":{uri}"), @@ -36,9 +28,9 @@ impl<'a> Encode<'a> { } match self.0.0.kind() { - SchemeKind::Regular => Ok(()), - SchemeKind::Search | SchemeKind::Archive => w!(0, 0), - SchemeKind::Sftp => { + AuthKind::Regular => Ok(()), + AuthKind::Search | AuthKind::Mount => w!(0, 0), + AuthKind::Scope | AuthKind::Sftp => { w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize) } } @@ -53,11 +45,12 @@ impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { Url::Regular(_) => write!(f, "regular://"), - Url::Search { domain, .. } => write!(f, "search://{}{}/", Self::domain(domain), self.ports()), - Url::Archive { domain, .. } => { - write!(f, "archive://{}{}/", Self::domain(domain), self.ports()) + Url::Search { auth, .. } + | Url::Mount { auth, .. } + | Url::Scope { auth, .. } + | Url::Sftp { auth, .. } => { + write!(f, "{}{}/", EncodeAuth(auth, false), self.ports()) } - Url::Sftp { domain, .. } => write!(f, "sftp://{}{}/", Self::domain(domain), self.ports()), } } } diff --git a/yazi-shared/src/spec/inventory.rs b/yazi-shared/src/spec/inventory.rs new file mode 100644 index 00000000..613ed014 --- /dev/null +++ b/yazi-shared/src/spec/inventory.rs @@ -0,0 +1,9 @@ +use mlua::UserDataRegistry; + +use crate::spec::Spec; + +pub struct SpecInventory { + pub register: fn(&mut UserDataRegistry), +} + +inventory::collect!(SpecInventory); diff --git a/yazi-shared/src/spec/lua.rs b/yazi-shared/src/spec/lua.rs new file mode 100644 index 00000000..7c5f34cf --- /dev/null +++ b/yazi-shared/src/spec/lua.rs @@ -0,0 +1,23 @@ +use mlua::{UserData, UserDataFields, UserDataRegistry}; +use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; + +use crate::{auth::AuthKind, spec::{Spec, SpecInventory}}; + +impl UserData for Spec { + fn add_fields>(fields: &mut F) { + fields.add_cached_field("kind", |_, me| Ok(me.kind.into_str())); + fields.add_cached_field("domain", |lua, me| lua.create_string(&*me.domain)); + fields.add_field_method_get("is_regular", |_, me| Ok(me.kind == AuthKind::Regular)); + fields.add_field_method_get("is_search", |_, me| Ok(me.kind == AuthKind::Search)); + fields.add_field_method_get("is_virtual", |_, me| Ok(me.kind.is_virtual())); + } + + fn register(registry: &mut UserDataRegistry) { + Self::add_fields(registry); + Self::add_methods(registry); + + for inv in inventory::iter::() { + (inv.register)(registry); + } + } +} diff --git a/yazi-shared/src/spec/mod.rs b/yazi-shared/src/spec/mod.rs new file mode 100644 index 00000000..22ef313e --- /dev/null +++ b/yazi-shared/src/spec/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(spec encode inventory lua parsed); diff --git a/yazi-shared/src/spec/parsed.rs b/yazi-shared/src/spec/parsed.rs new file mode 100644 index 00000000..aaf272d1 --- /dev/null +++ b/yazi-shared/src/spec/parsed.rs @@ -0,0 +1,43 @@ +use std::fmt; + +use anyhow::Result; + +use crate::{BytesExt, auth::Scheme}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ParsedSpec<'a> { + bytes: &'a [u8], + skip: usize, + pub scheme: Scheme, + pub tilde: bool, +} + +impl fmt::Display for ParsedSpec<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.scheme, if self.tilde { "~" } else { "" }) + } +} + +impl<'a> ParsedSpec<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + let Some((scheme, _)) = bytes.split_seq_once(b"://") else { + return Ok(Self { bytes, skip: 0, scheme: Scheme::Regular, tilde: false }); + }; + + let (scheme, tilde) = if let Some(stripped) = scheme.strip_suffix(b"~") { + (stripped, true) + } else { + (scheme, false) + }; + + let scheme: Scheme = str::from_utf8(scheme)?.parse()?; + let skip = 3 + scheme.as_str().len() + tilde as usize; + Ok(Self { bytes, skip, scheme, tilde }) + } + + #[inline] + pub fn has_scheme(&self) -> bool { self.skip > 0 } + + #[inline] + pub fn rest(&self) -> &'a [u8] { &self.bytes[self.skip..] } +} diff --git a/yazi-shared/src/spec/spec.rs b/yazi-shared/src/spec/spec.rs new file mode 100644 index 00000000..b33ad015 --- /dev/null +++ b/yazi-shared/src/spec/spec.rs @@ -0,0 +1,129 @@ +use std::{borrow::Cow, ops::Deref, sync::Arc}; + +use anyhow::{Result, anyhow, ensure}; +use percent_encoding::percent_decode; +use serde::Deserialize; + +use crate::{auth::{Auth, AuthKind, Encode, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url}; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct Spec { + #[serde(flatten)] + pub auth: Arc, + pub uri: usize, + pub urn: usize, +} + +impl Deref for Spec { + type Target = Auth; + + fn deref(&self) -> &Self::Target { &self.auth } +} + +impl Spec { + pub fn parse<'a>(bytes: &'a [u8]) -> Result<(Self, PathCow<'a>)> { + let parsed = ParsedSpec::parse(bytes)?; + let rest = parsed.rest(); + + // Decode domain and ports + let mut skip = 0; + let (domain, uri, urn) = match &parsed.scheme { + Scheme::Regular => (Cow::Borrowed(""), None, None), + _ => Self::decode_param(rest, &mut skip)?, + }; + + // Resolve authority + let auth = Auth::get(&parsed.scheme, &domain) + .ok_or_else(|| anyhow!("unknown VFS authority: {parsed}://{}", Encode::domain(&domain)))?; + + // Decode path + let path = Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?; + let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?; + + Ok((Self { auth, uri, urn }, path)) + } + + pub const fn ports(&self) -> (usize, usize) { (self.uri, self.urn) } + + #[inline] + pub fn with_ports(self, uri: usize, urn: usize) -> Self { Self { uri, urn, ..self } } + + #[inline] + pub fn zeroed(self) -> Self { self.with_ports(0, 0) } + + fn decode_param<'a>( + bytes: &'a [u8], + skip: &mut usize, + ) -> Result<(Cow<'a, str>, Option, Option)> { + let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); + let slash = bytes.get(len).is_some_and(|&b| b == b'/'); + *skip += len + slash as usize; + + let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; + let domain = match Cow::from(percent_decode(&bytes[..len])) { + Cow::Borrowed(b) => str::from_utf8(b)?.into(), + Cow::Owned(b) => String::from_utf8(b)?.into(), + }; + + Ok((domain, uri, urn)) + } + + fn decode_ports(bytes: &[u8], skip: &mut usize) -> Result<(Option, Option)> { + let Some(a_idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok((None, None)) }; + let a_len = bytes.len() - a_idx; + *skip -= a_len; + let a = if a_len == 1 { None } else { Some(str::from_utf8(&bytes[a_idx + 1..])?.parse()?) }; + + let Some(b_idx) = bytes[..a_idx].iter().rposition(|&b| b == b':') else { + return Ok((a, None)); + }; + let b_len = bytes[..a_idx].len() - b_idx; + *skip -= b_len; + let b = + if b_len == 1 { None } else { Some(str::from_utf8(&bytes[b_idx + 1..a_idx])?.parse()?) }; + + Ok((b, a)) + } + + fn decode_path<'a>(kind: AuthKind, tilde: bool, bytes: &'a [u8]) -> Result> { + let bytes: Cow<_> = if tilde { percent_decode(bytes).into() } else { bytes.into() }; + PathCow::with(kind, bytes) + } + + fn normalize_ports( + kind: AuthKind, + uri: Option, + urn: Option, + path: &PathCow, + ) -> Result<(usize, usize)> { + Ok(match kind { + AuthKind::Regular => { + ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); + (path.name().is_some() as usize, path.name().is_some() as usize) + } + AuthKind::Search => { + let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); + ensure!(uri == urn, "Search scheme requires URI and URN to be equal"); + (uri, urn) + } + AuthKind::Mount => (uri.unwrap_or(0), urn.unwrap_or(0)), + AuthKind::Scope | AuthKind::Sftp => { + let uri = uri.unwrap_or(path.name().is_some() as usize); + let urn = urn.unwrap_or(path.name().is_some() as usize); + (uri, urn) + } + }) + } + + pub fn retrieve_ports(url: Url) -> (usize, usize) { + match url { + Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), + Url::Search { loc, .. } | Url::Mount { loc, .. } => { + (loc.uri().components().count(), loc.urn().components().count()) + } + Url::Scope { loc, .. } | Url::Sftp { loc, .. } => { + (loc.uri().components().count(), loc.urn().components().count()) + } + } + } +} diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index 103afc20..7ed18e20 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -1,4 +1,4 @@ -use crate::{path::PathKind, scheme::SchemeKind}; +use crate::{auth::AuthKind, path::PathKind}; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum StrandKind { @@ -16,13 +16,14 @@ impl From for StrandKind { } } -impl From for StrandKind { - fn from(value: SchemeKind) -> Self { +impl From for StrandKind { + fn from(value: AuthKind) -> Self { match value { - SchemeKind::Regular => Self::Os, - SchemeKind::Search => Self::Os, - SchemeKind::Archive => Self::Os, - SchemeKind::Sftp => Self::Bytes, + AuthKind::Regular => Self::Os, + AuthKind::Search => Self::Os, + AuthKind::Mount => Self::Os, + AuthKind::Scope => Self::Bytes, + AuthKind::Sftp => Self::Bytes, } } } diff --git a/yazi-shared/src/tests.rs b/yazi-shared/src/tests.rs index 0dedad2b..0ac25d82 100644 --- a/yazi-shared/src/tests.rs +++ b/yazi-shared/src/tests.rs @@ -1,7 +1,20 @@ -pub fn init_tests() { - static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new(); +use std::sync::OnceLock; - INIT.get_or_init(|| { - crate::init(); - }); +use crate::auth::{Auth, AuthInventory, AuthKind}; + +pub fn init_tests() { + static INIT: OnceLock<()> = OnceLock::new(); + + INIT.get_or_init(crate::init); +} + +inventory::submit! { + AuthInventory { + get: |scheme, domain| match (scheme.as_str(), domain) { + ("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), + ("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), + ("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), + _ => None, + }, + } } diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index ef9b2e05..b86f4f4f 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,18 +1,19 @@ -use std::{borrow::Cow, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, path::{Path, PathBuf}, str::FromStr}; +use std::{borrow::Cow, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, path::{Path, PathBuf}, str::FromStr, sync::Arc}; use anyhow::Result; use serde::{Deserialize, Serialize, de::{self, IntoDeserializer}}; use yazi_codegen::FromLuaOwned; use yazi_macro::impl_data_any; -use crate::{loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::{Scheme, SchemeLike}, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; +use crate::{auth::Auth, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; #[derive(Clone, Eq, FromLuaOwned)] pub enum UrlBuf { Regular(LocBuf), - Search { loc: LocBuf, domain: Symbol }, - Archive { loc: LocBuf, domain: Symbol }, - Sftp { loc: LocBuf, domain: Symbol }, + Search { loc: LocBuf, auth: Arc }, + Mount { loc: LocBuf, auth: Arc }, + Scope { loc: LocBuf, auth: Arc }, + Sftp { loc: LocBuf, auth: Arc }, } impl_data_any!(UrlBuf); @@ -30,9 +31,10 @@ impl From> for UrlBuf { fn from(url: Url<'_>) -> Self { match url { Url::Regular(loc) => Self::Regular(loc.into()), - Url::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.intern() }, - Url::Archive { loc, domain } => Self::Archive { loc: loc.into(), domain: domain.intern() }, - Url::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.intern() }, + Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, + Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, + Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, + Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, } } } @@ -71,10 +73,10 @@ impl TryFrom for UrlBuf { } } -impl TryFrom<(Scheme, PathBufDyn)> for UrlBuf { +impl TryFrom<(Spec, PathBufDyn)> for UrlBuf { type Error = anyhow::Error; - fn try_from(value: (Scheme, PathBufDyn)) -> Result { + fn try_from(value: (Spec, PathBufDyn)) -> Result { Ok(UrlCow::try_from(value)?.into_owned()) } } @@ -137,7 +139,8 @@ impl UrlBuf { match self { Self::Regular(loc) => loc.into_inner().into(), Self::Search { loc, .. } => loc.into_inner().into(), - Self::Archive { loc, .. } => loc.into_inner().into(), + Self::Mount { loc, .. } => loc.into_inner().into(), + Self::Scope { loc, .. } => loc.into_inner().into(), Self::Sftp { loc, .. } => loc.into_inner().into(), } } @@ -152,7 +155,8 @@ impl UrlBuf { Ok(match self { Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, - Self::Archive { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Mount { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Scope { loc, .. } => loc.try_set_name(name.encoded_bytes())?, Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?, }) } @@ -160,16 +164,10 @@ impl UrlBuf { pub fn rebase(&self, base: &Path) -> Self { match self { Self::Regular(loc) => Self::Regular(loc.rebase(base)), - Self::Search { loc, domain } => { - Self::Search { loc: loc.rebase(base), domain: domain.clone() } - } - Self::Archive { loc, domain } => { - Self::Archive { loc: loc.rebase(base), domain: domain.clone() } - } - Self::Sftp { loc, domain } => { - todo!(); - // Self::Sftp { loc: loc.rebase(base), domain: domain.clone() } - } + Self::Search { loc, auth } => Self::Search { loc: loc.rebase(base), auth: auth.clone() }, + Self::Mount { loc, auth } => Self::Mount { loc: loc.rebase(base), auth: auth.clone() }, + Self::Scope { .. } => todo!(), + Self::Sftp { .. } => todo!(), } } } @@ -184,10 +182,10 @@ impl UrlBuf { } #[inline] - pub fn into_search(self, domain: impl AsRef) -> Result { + pub fn into_search(self, query: impl AsRef) -> Result { Ok(Self::Search { - loc: LocBuf::::zeroed(self.into_loc().into_os()?), - domain: Pool::::intern(domain), + loc: LocBuf::::zeroed(self.into_loc().into_os()?), + auth: Auth::search(query.as_ref()), }) } } @@ -237,14 +235,14 @@ impl<'de> Deserialize<'de> for UrlBuf { #[derive(Deserialize)] struct Shadow { #[serde(flatten)] - scheme: Scheme, - path: Vec, + spec: Spec, + path: Vec, } - let Shadow { scheme, path } = Deserialize::deserialize(deserializer)?; - let path = PathBufDyn::with(scheme.kind(), path).map_err(de::Error::custom)?; + let Shadow { spec, path } = Deserialize::deserialize(deserializer)?; + let path = PathBufDyn::with(spec.kind, path).map_err(de::Error::custom)?; - UrlBuf::try_from((scheme, path)).map_err(de::Error::custom) + UrlBuf::try_from((spec, path)).map_err(de::Error::custom) } } @@ -281,13 +279,13 @@ mod tests { // Search ("search://kw//a", "b/c", "search://kw:2:2//a/b/c"), ("search://kw:2:2//a/b/c", "d/e", "search://kw:4:4//a/b/c/d/e"), - // Archive - ("archive:////a/b.zip", "c/d", "archive://:2:1//a/b.zip/c/d"), - ("archive://:2:1//a/b.zip/c/d", "e/f", "archive://:4:1//a/b.zip/c/d/e/f"), - ("archive://:2:2//a/b.zip/c/d", "e/f", "archive://:4:1//a/b.zip/c/d/e/f"), + // Mount + ("test-mount://7z//a/b.zip", "c/d", "test-mount://7z:2:1//a/b.zip/c/d"), + ("test-mount://7z:2:1//a/b.zip/c/d", "e/f", "test-mount://7z:4:1//a/b.zip/c/d/e/f"), + ("test-mount://7z:2:2//a/b.zip/c/d", "e/f", "test-mount://7z:4:1//a/b.zip/c/d/e/f"), // SFTP - ("sftp://remote//a", "b/c", "sftp://remote//a/b/c"), - ("sftp://remote:1:1//a/b/c", "d/e", "sftp://remote//a/b/c/d/e"), + ("sftp://vps//a", "b/c", "sftp://vps//a/b/c"), + ("sftp://vps:1:1//a/b/c", "d/e", "sftp://vps//a/b/c/d/e"), // Relative ("search://kw", "b/c", "search://kw:2:2/b/c"), ("search://kw/", "b/c", "search://kw:2:2/b/c"), @@ -318,17 +316,17 @@ mod tests { ("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")), ("search://kw:1:1//a/b", Some("search://kw//a")), ("search://kw//a", Some("/")), - // Archive - ("archive://:2:1//a/b.zip/c/d", Some("archive://:1:1//a/b.zip/c")), - ("archive://:1:1//a/b.zip/c", Some("archive:////a/b.zip")), - ("archive:////a/b.zip", Some("/a")), + // Mount + ("test-mount://7z:2:1//a/b.zip/c/d", Some("test-mount://7z:1:1//a/b.zip/c")), + ("test-mount://7z:1:1//a/b.zip/c", Some("test-mount://7z//a/b.zip")), + ("test-mount://7z//a/b.zip", Some("/a")), // SFTP - ("sftp://remote:3:1//a/b", Some("sftp://remote//a")), - ("sftp://remote:2:1//a", Some("sftp://remote//")), - ("sftp://remote:1:1//a", Some("sftp://remote//")), - ("sftp://remote//a", Some("sftp://remote//")), - ("sftp://remote:1//", None), - ("sftp://remote//", None), + ("sftp://vps:3:1//a/b", Some("sftp://vps//a")), + ("sftp://vps:2:1//a", Some("sftp://vps//")), + ("sftp://vps:1:1//a", Some("sftp://vps//")), + ("sftp://vps//a", Some("sftp://vps//")), + ("sftp://vps:1//", None), + ("sftp://vps//", None), // Relative ("search://kw:2:2/a/b", Some("search://kw:1:1/a")), ("search://kw:1:1/a", None), diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index 37835a61..6277737b 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -2,11 +2,11 @@ use std::path::PrefixComponent; use anyhow::Result; -use crate::{path::{self, PathDynError}, scheme::SchemeRef, strand::Strand}; +use crate::{auth::Auth, path::{self, PathDynError}, strand::Strand}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Component<'a> { - Scheme(SchemeRef<'a>), + Auth(&'a Auth), Prefix(PrefixComponent<'a>), RootDir, CurDir, @@ -29,7 +29,7 @@ impl<'a> From> for Component<'a> { impl<'a> Component<'a> { pub fn downgrade(self) -> Option> { Some(match self { - Self::Scheme(_) => None?, + Self::Auth(_) => None?, Self::Prefix(p) => path::Component::Prefix(p), Self::RootDir => path::Component::RootDir, Self::CurDir => path::Component::CurDir, @@ -44,7 +44,7 @@ impl<'a> Component<'a> { // let mut buf = PathBuf::new(); // let mut scheme = None; // iter.into_iter().for_each(|c| match c { -// Component::Scheme(s) => scheme = Some(s), +// Component::Auth(s) => scheme = Some(s), // Component::Prefix(p) => buf.push(path::Component::Prefix(p)), // Component::RootDir => buf.push(path::Component::RootDir), // Component::CurDir => buf.push(path::Component::CurDir), diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs index 50778f81..ed7f2cfe 100644 --- a/yazi-shared/src/url/components.rs +++ b/yazi-shared/src/url/components.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not}; -use crate::{loc::Loc, path, scheme::{Encode as EncodeScheme, SchemeCow, SchemeRef}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; +use crate::{auth::Auth, loc::Loc, path, spec::{Encode as EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; #[derive(Clone)] pub struct Components<'a> { @@ -25,7 +25,7 @@ impl<'a> Components<'a> { pub fn covariant(&self, other: &Self) -> bool { match (self.scheme_yielded, other.scheme_yielded) { (true, true) => {} - (false, false) if self.scheme().covariant(other.scheme()) => {} + (false, false) if self.auth().covariant(other.auth()) => {} _ => return false, } self.inner == other.inner @@ -40,27 +40,24 @@ impl<'a> Components<'a> { return os.into(); } - let mut s = OsString::from(EncodeScheme(self.url()).to_string()); + let mut s = OsString::from(EncodeSpec(self.url()).to_string()); s.reserve_exact(os.len()); s.push(os); s.into() } - pub fn scheme(&self) -> SchemeRef<'a> { + pub fn auth(&self) -> &'a Auth { self.url.auth() } + + fn ports(&self) -> (usize, usize) { let left = self.inner.clone().count(); - let (uri, urn) = SchemeCow::retrieve_ports(self.url); + let (uri, urn) = Spec::retrieve_ports(self.url); let (uri, urn) = ( uri.saturating_sub(self.back_yields).min(left), urn.saturating_sub(self.back_yields).min(left), ); - match self.url { - Url::Regular(_) => SchemeRef::Regular { uri, urn }, - Url::Search { domain, .. } => SchemeRef::Search { domain, uri, urn }, - Url::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn }, - Url::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn }, - } + (uri, urn) } pub fn strand(&self) -> StrandCow<'a> { @@ -69,7 +66,7 @@ impl<'a> Components<'a> { return s.into(); } - let mut buf = StrandBuf::with_str(s.kind(), EncodeScheme(self.url()).to_string()); + let mut buf = StrandBuf::with_str(s.kind(), EncodeSpec(self.url()).to_string()); buf.reserve_exact(s.len()); buf.try_push(s).expect("strand with same kind"); buf.into() @@ -77,17 +74,20 @@ impl<'a> Components<'a> { pub fn url(&self) -> Url<'a> { let path = self.inner.path(); - let (uri, urn) = self.scheme().ports(); + let (uri, urn) = self.ports(); match self.url { Url::Regular(_) => Url::Regular(Loc::with(path.as_os().unwrap(), uri, urn).unwrap()), - Url::Search { domain, .. } => { - Url::Search { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } + Url::Search { auth, .. } => { + Url::Search { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), auth } } - Url::Archive { domain, .. } => { - Url::Archive { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } + Url::Mount { auth, .. } => { + Url::Mount { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), auth } } - Url::Sftp { domain, .. } => { - Url::Sftp { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), domain } + Url::Scope { auth, .. } => { + Url::Scope { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), auth } + } + Url::Sftp { auth, .. } => { + Url::Sftp { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), auth } } } } @@ -99,7 +99,7 @@ impl<'a> Iterator for Components<'a> { fn next(&mut self) -> Option { if !self.scheme_yielded { self.scheme_yielded = true; - Some(Component::Scheme(self.scheme())) + Some(Component::Auth(self.auth())) } else { self.inner.next().map(Into::into) } @@ -120,7 +120,7 @@ impl<'a> DoubleEndedIterator for Components<'a> { Some(c.into()) } else if !self.scheme_yielded { self.scheme_yielded = true; - Some(Component::Scheme(self.scheme())) + Some(Component::Auth(self.auth())) } else { None } @@ -136,7 +136,7 @@ impl<'a> PartialEq for Components<'a> { } match (self.scheme_yielded, other.scheme_yielded) { (true, true) => true, - (false, false) if self.scheme() == other.scheme() => true, + (false, false) if self.auth() == other.auth() => true, _ => false, } } @@ -147,49 +147,49 @@ impl<'a> PartialEq for Components<'a> { mod tests { use anyhow::Result; - use crate::{scheme::SchemeRef, url::{Component, UrlBuf, UrlLike}}; + use crate::{auth::Auth as A, spec::Spec as D, url::{Component, UrlBuf, UrlLike}}; #[test] fn test_url() -> Result<()> { use Component::*; - use SchemeRef as S; crate::init_tests(); + let s = |uri, urn| D { auth: A::search("keyword"), uri, urn }; let search: UrlBuf = "search://keyword//root/projects/yazi".parse()?; assert_eq!(search.uri(), ""); - assert_eq!(search.scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(search.spec(), s(0, 0)); let src = search.try_join("src")?; assert_eq!(src.uri(), "src"); - assert_eq!(src.scheme(), S::Search { domain: "keyword", uri: 1, urn: 1 }); + assert_eq!(src.spec(), s(1, 1)); let main = src.try_join("main.rs")?; assert_eq!(main.urn(), "src/main.rs"); - assert_eq!(main.scheme(), S::Search { domain: "keyword", uri: 2, urn: 2 }); + assert_eq!(main.spec(), s(2, 2)); let mut it = main.components(); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 2, urn: 2 }); + assert_eq!(it.url().spec(), s(2, 2)); assert_eq!(it.next_back(), Some(Normal("main.rs".into()))); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 1, urn: 1 }); + assert_eq!(it.url().spec(), s(1, 1)); assert_eq!(it.next_back(), Some(Normal("src".into()))); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(it.url().spec(), s(0, 0)); assert_eq!(it.next_back(), Some(Normal("yazi".into()))); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(it.url().spec(), s(0, 0)); let mut it = main.components(); - assert_eq!(it.next(), Some(Scheme(S::Search { domain: "keyword", uri: 2, urn: 2 }))); + assert_eq!(it.next(), Some(Auth(&A::search("keyword")))); assert_eq!(it.next(), Some(RootDir)); assert_eq!(it.next(), Some(Normal("root".into()))); assert_eq!(it.next(), Some(Normal("projects".into()))); assert_eq!(it.next(), Some(Normal("yazi".into()))); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 2, urn: 2 }); + assert_eq!(it.url().spec(), s(2, 2)); assert_eq!(it.next(), Some(Normal("src".into()))); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 1, urn: 1 }); + assert_eq!(it.url().spec(), s(1, 1)); assert_eq!(it.next_back(), Some(Normal("main.rs".into()))); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(it.url().spec(), s(0, 0)); assert_eq!(it.next(), None); - assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(it.url().spec(), s(0, 0)); Ok(()) } diff --git a/yazi-shared/src/url/cov.rs b/yazi-shared/src/url/cov.rs index 7b77c24b..b88e17af 100644 --- a/yazi-shared/src/url/cov.rs +++ b/yazi-shared/src/url/cov.rs @@ -34,7 +34,7 @@ impl Hash for UrlCov<'_> { fn hash(&self, state: &mut H) { self.0.loc().hash(state); if self.0.kind().is_virtual() { - self.0.scheme().hash(state); + self.0.auth().hash(state); } } } diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 323e34fb..4b09a528 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -1,31 +1,28 @@ -use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf}; +use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf, sync::Arc}; -use anyhow::{Result, anyhow}; +use anyhow::Result; use serde::{Deserialize, Deserializer, Serialize}; use typed_path::{UnixPath, UnixPathBuf}; -use crate::{loc::{Loc, LocBuf, LocCow}, path::{PathBufDyn, PathCow, PathDyn}, pool::SymbolCow, scheme::{AsScheme, Scheme, SchemeCow, SchemeKind, SchemeRef}, url::{AsUrl, Url, UrlBuf}}; +use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}}; #[derive(Clone, Debug)] pub enum UrlCow<'a> { Regular(LocCow<'a>), - Search { loc: LocCow<'a>, domain: SymbolCow<'a, str> }, - Archive { loc: LocCow<'a>, domain: SymbolCow<'a, str> }, - Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, domain: SymbolCow<'a, str> }, -} - -// FIXME: remove -impl Default for UrlCow<'_> { - fn default() -> Self { Self::Regular(Default::default()) } + Search { loc: LocCow<'a>, auth: Arc }, + Mount { loc: LocCow<'a>, auth: Arc }, + Scope { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc }, + Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc }, } impl<'a> From> for UrlCow<'a> { fn from(value: Url<'a>) -> Self { match value { Url::Regular(loc) => Self::Regular(loc.into()), - Url::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.into() }, - Url::Archive { loc, domain } => Self::Archive { loc: loc.into(), domain: domain.into() }, - Url::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.into() }, + Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, + Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, + Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, + Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, } } } @@ -41,11 +38,10 @@ impl From for UrlCow<'_> { fn from(value: UrlBuf) -> Self { match value { UrlBuf::Regular(loc) => Self::Regular(loc.into()), - UrlBuf::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.into() }, - UrlBuf::Archive { loc, domain } => { - Self::Archive { loc: loc.into(), domain: domain.into() } - } - UrlBuf::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.into() }, + UrlBuf::Search { loc, auth } => Self::Search { loc: loc.into(), auth }, + UrlBuf::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth }, + UrlBuf::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth }, + UrlBuf::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth }, } } } @@ -65,7 +61,7 @@ impl From<&UrlCow<'_>> for UrlBuf { impl<'a> TryFrom<&'a [u8]> for UrlCow<'a> { type Error = anyhow::Error; - fn try_from(value: &'a [u8]) -> Result { SchemeCow::parse(value)?.try_into() } + fn try_from(value: &'a [u8]) -> Result { Spec::parse(value)?.try_into() } } impl TryFrom> for UrlCow<'_> { @@ -101,89 +97,55 @@ impl<'a> TryFrom> for UrlCow<'a> { } } -impl<'a> TryFrom<(SchemeRef<'a>, PathDyn<'a>)> for UrlCow<'a> { +impl<'a> TryFrom<(Spec, PathCow<'a>)> for UrlCow<'a> { type Error = anyhow::Error; - fn try_from((scheme, path): (SchemeRef<'a>, PathDyn<'a>)) -> Result { - (SchemeCow::Borrowed(scheme), path).try_into() - } -} - -impl<'a> TryFrom<(SchemeRef<'a>, PathCow<'a>)> for UrlCow<'a> { - type Error = anyhow::Error; - - fn try_from((scheme, path): (SchemeRef<'a>, PathCow<'a>)) -> Result { - (SchemeCow::Borrowed(scheme), path).try_into() - } -} - -impl TryFrom<(Scheme, PathBufDyn)> for UrlCow<'_> { - type Error = anyhow::Error; - - fn try_from((scheme, path): (Scheme, PathBufDyn)) -> Result { - (SchemeCow::Owned(scheme), path).try_into() - } -} - -impl<'a> TryFrom<(SchemeCow<'a>, PathCow<'a>)> for UrlCow<'a> { - type Error = anyhow::Error; - - fn try_from((scheme, path): (SchemeCow<'a>, PathCow<'a>)) -> Result { + fn try_from((spec, path): (Spec, PathCow<'a>)) -> Result { match path { - PathCow::Borrowed(path) => (scheme, path).try_into(), - PathCow::Owned(path) => (scheme, path).try_into(), + PathCow::Borrowed(path) => (spec, path).try_into(), + PathCow::Owned(path) => (spec, path).try_into(), } } } -impl<'a> TryFrom<(SchemeCow<'a>, PathDyn<'a>)> for UrlCow<'a> { +impl<'a> TryFrom<(Spec, PathDyn<'a>)> for UrlCow<'a> { type Error = anyhow::Error; - fn try_from((scheme, path): (SchemeCow<'a>, PathDyn<'a>)) -> Result { - let kind = scheme.as_scheme().kind(); - let (uri, urn) = scheme.as_scheme().ports(); - let domain = scheme.into_domain(); - Ok(match kind { - SchemeKind::Regular => Self::Regular(Loc::bare(path.as_os()?).into()), - SchemeKind::Search => Self::Search { - loc: Loc::with(path.as_os()?, uri, urn)?.into(), - domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, - }, - SchemeKind::Archive => Self::Archive { - loc: Loc::with(path.as_os()?, uri, urn)?.into(), - domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, - }, - SchemeKind::Sftp => Self::Sftp { - loc: Loc::with(path.as_unix()?, uri, urn)?.into(), - domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, - }, + fn try_from((spec, path): (Spec, PathDyn<'a>)) -> Result { + let Spec { auth, uri, urn } = spec; + Ok(match auth.kind { + AuthKind::Regular => Self::Regular(Loc::bare(path.as_os()?).into()), + AuthKind::Search => Self::Search { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, + AuthKind::Mount => Self::Mount { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, + AuthKind::Scope => Self::Scope { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, + AuthKind::Sftp => Self::Sftp { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, }) } } -impl<'a> TryFrom<(SchemeCow<'a>, PathBufDyn)> for UrlCow<'a> { +impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> { type Error = anyhow::Error; - fn try_from((scheme, path): (SchemeCow<'a>, PathBufDyn)) -> Result { - let kind = scheme.as_scheme().kind(); - let (uri, urn) = scheme.as_scheme().ports(); - let domain = scheme.into_domain(); - Ok(match kind { - SchemeKind::Regular => { + fn try_from((spec, path): (Spec, PathBufDyn)) -> Result { + let Spec { auth, uri, urn } = spec; + Ok(match auth.kind { + AuthKind::Regular => { Self::Regular(LocBuf::::from(path.into_os()?).into()) } - SchemeKind::Search => Self::Search { - loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), - domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, + AuthKind::Search => Self::Search { + loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), + auth, }, - SchemeKind::Archive => Self::Archive { - loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), - domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, - }, - SchemeKind::Sftp => Self::Sftp { - loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), - domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, + AuthKind::Mount => Self::Mount { + loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), + auth, }, + AuthKind::Scope => { + Self::Scope { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth } + } + AuthKind::Sftp => { + Self::Sftp { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth } + } }) } } @@ -209,7 +171,8 @@ impl<'a> UrlCow<'a> { match self { Self::Regular(loc) => loc.is_owned(), Self::Search { loc, .. } => loc.is_owned(), - Self::Archive { loc, .. } => loc.is_owned(), + Self::Mount { loc, .. } => loc.is_owned(), + Self::Scope { loc, .. } => loc.is_owned(), Self::Sftp { loc, .. } => loc.is_owned(), } } @@ -217,47 +180,24 @@ impl<'a> UrlCow<'a> { pub fn into_owned(self) -> UrlBuf { match self { Self::Regular(loc) => UrlBuf::Regular(loc.into_owned()), - Self::Search { loc, domain } => { - UrlBuf::Search { loc: loc.into_owned(), domain: domain.into() } - } - Self::Archive { loc, domain } => { - UrlBuf::Archive { loc: loc.into_owned(), domain: domain.into() } - } - Self::Sftp { loc, domain } => { - UrlBuf::Sftp { loc: loc.into_owned(), domain: domain.into() } - } + Self::Search { loc, auth } => UrlBuf::Search { loc: loc.into_owned(), auth }, + Self::Mount { loc, auth } => UrlBuf::Mount { loc: loc.into_owned(), auth }, + Self::Scope { loc, auth } => UrlBuf::Scope { loc: loc.into_owned(), auth }, + Self::Sftp { loc, auth } => UrlBuf::Sftp { loc: loc.into_owned(), auth }, } } - pub fn into_pair(self) -> (SchemeCow<'a>, PathCow<'a>) { - let (uri, urn) = self.as_url().scheme().ports(); - match self { - Self::Regular(loc) => match loc { - LocCow::Borrowed(_) => (SchemeRef::Regular { uri, urn }.into(), loc.into_path()), - LocCow::Owned(_) => (Scheme::Regular { uri, urn }.into(), loc.into_path()), - }, - Self::Search { loc, domain } => match domain { - SymbolCow::Borrowed(domain) => { - (SchemeRef::Search { domain, uri, urn }.into(), loc.into_path()) - } - SymbolCow::Owned(domain) => (Scheme::Search { domain, uri, urn }.into(), loc.into_path()), - }, - Self::Archive { loc, domain } => match domain { - SymbolCow::Borrowed(domain) => { - (SchemeRef::Archive { domain, uri, urn }.into(), loc.into_path()) - } - SymbolCow::Owned(domain) => (Scheme::Archive { domain, uri, urn }.into(), loc.into_path()), - }, - Self::Sftp { loc, domain } => match domain { - SymbolCow::Borrowed(domain) => { - (SchemeRef::Sftp { domain, uri, urn }.into(), loc.into_path()) - } - SymbolCow::Owned(domain) => (Scheme::Sftp { domain, uri, urn }.into(), loc.into_path()), - }, - } + pub fn into_pair(self) -> (Spec, PathCow<'a>) { + let (uri, urn) = Spec::retrieve_ports(self.as_url()); + let (auth, path) = match self { + Self::Regular(loc) => (Auth::default_arc(), loc.into_path()), + Self::Search { loc, auth } | Self::Mount { loc, auth } => (auth, loc.into_path()), + Self::Scope { loc, auth } | Self::Sftp { loc, auth } => (auth, loc.into_path()), + }; + (Spec { auth, uri, urn }, path) } - pub fn into_scheme(self) -> SchemeCow<'a> { self.into_pair().0 } + pub fn into_spec(self) -> Spec { self.into_pair().0 } pub fn into_path(self) -> PathCow<'a> { self.into_pair().1 } @@ -289,6 +229,8 @@ mod tests { #[test] fn test_parse() -> Result<()> { + crate::init_tests(); + struct Case { url: &'static str, urn: &'static str, @@ -322,29 +264,29 @@ mod tests { trail: "search://keyword//root/Documents/reports/", base: "search://keyword//root/Documents/reports/", }, - // Archive portal + // Mount portal Case { - url: "archive://domain//root/Downloads/images.zip", + url: "test-mount://7z//root/Downloads/images.zip", urn: "", uri: "", - trail: "archive://domain//root/Downloads/images.zip", - base: "archive://domain//root/Downloads/images.zip", + trail: "test-mount://7z//root/Downloads/images.zip", + base: "test-mount://7z//root/Downloads/images.zip", }, - // Archive item + // Mount item Case { - url: "archive://domain:2:1//root/Downloads/images.zip/2025/city.jpg", + url: "test-mount://7z:2:1//root/Downloads/images.zip/2025/city.jpg", urn: "city.jpg", uri: "2025/city.jpg", - trail: "archive://domain:1:1//root/Downloads/images.zip/2025/", - base: "archive://domain//root/Downloads/images.zip/", + trail: "test-mount://7z:1:1//root/Downloads/images.zip/2025/", + base: "test-mount://7z//root/Downloads/images.zip/", }, // SFTP Case { - url: "sftp://my-server//root/docs/report.pdf", + url: "sftp://vps//root/docs/report.pdf", urn: "report.pdf", uri: "report.pdf", - trail: "sftp://my-server//root/docs/", - base: "sftp://my-server//root/docs/", + trail: "sftp://vps//root/docs/", + base: "sftp://vps//root/docs/", }, ]; diff --git a/yazi-shared/src/url/de.rs b/yazi-shared/src/url/de.rs index 306b0f0d..d3449501 100644 --- a/yazi-shared/src/url/de.rs +++ b/yazi-shared/src/url/de.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; -use crate::{data::BytesDeserializer, pool::SymbolCow, scheme::SchemeLike, url::UrlCow}; +use crate::{data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>); @@ -32,7 +32,8 @@ impl<'de, 'a: 'de> Deserializer<'de> for UrlDeserializer<'a> { // --- Map struct MapDeserializer<'a> { kind: Option<&'static str>, - domain: Option>, + scheme: Option>, + domain: Option>, uri: Option, urn: Option, path: Option>, @@ -40,13 +41,13 @@ struct MapDeserializer<'a> { impl<'a> MapDeserializer<'a> { fn new(url: UrlCow<'a>) -> Self { - let (scheme, path) = url.into_pair(); - let kind: &'static str = scheme.kind().into(); - let (uri, urn) = scheme.ports(); + let (spec, path) = url.into_pair(); + let (uri, urn) = spec.ports(); Self { - kind: Some(kind), - domain: scheme.into_domain(), + kind: Some(spec.kind.into()), + scheme: Some(spec.scheme.intern()), + domain: Some(spec.domain.intern()), uri: Some(uri), urn: Some(urn), path: Some(path.into_encoded_bytes()), @@ -63,6 +64,8 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { { let key = if self.kind.is_some() { Some("kind") + } else if self.scheme.is_some() { + Some("scheme") } else if self.domain.is_some() { Some("domain") } else if self.uri.is_some() { @@ -85,8 +88,11 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { if let Some(kind) = self.kind.take() { return seed.deserialize(kind.into_deserializer()); } + if let Some(scheme) = self.scheme.take() { + return seed.deserialize(scheme.into_deserializer()); + } if let Some(domain) = self.domain.take() { - return seed.deserialize(domain.as_ref().into_deserializer()); + return seed.deserialize(domain.into_deserializer()); } if let Some(uri) = self.uri.take() { return seed.deserialize(uri.into_deserializer()); @@ -104,6 +110,7 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { fn size_hint(&self) -> Option { Some( self.kind.is_some() as usize + + self.scheme.is_some() as usize + self.domain.is_some() as usize + self.uri.is_some() as usize + self.urn.is_some() as usize diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index ad797709..85e43a76 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::{scheme::Encode, url::Url}; +use crate::{spec::Encode, url::Url}; pub struct Display<'a>(pub Url<'a>); diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 1695e50e..8969547d 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -2,26 +2,23 @@ use std::fmt::{self, Display}; use percent_encoding::{CONTROLS, percent_encode}; -use crate::url::Url; +use crate::{auth::Encode as EncodeAuth, url::Url}; #[derive(Clone, Copy)] pub struct Encode<'a>(pub Url<'a>); impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use crate::scheme::Encode as E; + use crate::spec::Encode as E; let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS); match self.0 { Url::Regular(_) => write!(f, "regular~://{loc}"), - Url::Search { domain, .. } => { - write!(f, "search~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) - } - Url::Archive { domain, .. } => { - write!(f, "archive~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) - } - Url::Sftp { domain, .. } => { - write!(f, "sftp~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) + Url::Search { auth, .. } + | Url::Mount { auth, .. } + | Url::Scope { auth, .. } + | Url::Sftp { auth, .. } => { + write!(f, "{}{}/{loc}", EncodeAuth(auth, true), E::ports((*self).into())) } } } diff --git a/yazi-shared/src/url/like.rs b/yazi-shared/src/url/like.rs index 48e7b847..471c808b 100644 --- a/yazi-shared/src/url/like.rs +++ b/yazi-shared/src/url/like.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, path::Path}; use anyhow::Result; -use crate::{path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; +use crate::{auth::{Auth, AuthKind}, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; pub trait UrlLike where @@ -10,6 +10,8 @@ where { fn as_local(&self) -> Option<&Path> { self.as_url().as_local() } + fn auth(&self) -> &Auth { self.as_url().auth() } + fn base(&self) -> Url<'_> { self.as_url().base() } fn components(&self) -> Components<'_> { self.as_url().into() } @@ -28,15 +30,11 @@ where fn is_absolute(&self) -> bool { self.as_url().is_absolute() } - fn is_archive(&self) -> bool { self.as_url().is_archive() } - - fn is_internal(&self) -> bool { self.as_url().is_internal() } - fn is_regular(&self) -> bool { self.as_url().is_regular() } fn is_search(&self) -> bool { self.as_url().is_search() } - fn kind(&self) -> SchemeKind { self.as_url().kind() } + fn kind(&self) -> AuthKind { self.as_url().kind() } fn loc(&self) -> PathDyn<'_> { self.as_url().loc() } @@ -48,7 +46,7 @@ where fn parent(&self) -> Option> { self.as_url().parent() } - fn scheme(&self) -> SchemeRef<'_> { self.as_url().scheme() } + fn spec(&self) -> Spec { self.as_url().spec() } fn stem(&self) -> Option> { self.as_url().stem() } diff --git a/yazi-shared/src/url/lua.rs b/yazi-shared/src/url/lua.rs index 559a5fff..65ef2086 100644 --- a/yazi-shared/src/url/lua.rs +++ b/yazi-shared/src/url/lua.rs @@ -1,7 +1,7 @@ use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; use yazi_shim::mlua::UserDataFieldsExt; -use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, scheme::{SchemeCow, SchemeLike}, strand::{StrandLike, ToStrand}, url::{UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; +use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, spec::Spec, strand::{StrandLike, ToStrand}, url::{UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; pub type UrlRef = UserDataRef; @@ -41,16 +41,16 @@ impl UrlBuf { match other { Value::String(s) => { let b = s.as_bytes(); - let (scheme, path) = SchemeCow::parse(&b)?; - if scheme.covariant(self.scheme()) { + let (spec, path) = Spec::parse(&b)?; + if spec.covariant(self.auth()) { self.try_join(path).into_lua_err()?.into_lua(lua) } else { - UrlCow::try_from((scheme, path))?.into_owned().into_lua(lua) + UrlCow::try_from((spec, path))?.into_owned().into_lua(lua) } } Value::UserData(ref ud) => { if let Ok(url) = ud.borrow::() { - if url.scheme().covariant(self.scheme()) { + if url.auth().covariant(self.auth()) { self.try_join(url.loc()).into_lua_err()?.into_lua(lua) } else { Ok(other) @@ -106,14 +106,8 @@ impl UserData for UrlBuf { }); fields.add_cached_field("parent", |_, me| Ok(me.parent().map(Self::from))); - fields.add_cached_field("scheme", |_, me| Ok(me.scheme().to_owned())); - fields.add_cached_field("domain", |lua, me| { - me.scheme().domain().map(|s| lua.create_string(s)).transpose() - }); + fields.add_cached_field("spec", |_, me| Ok(me.spec())); - fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); - fields.add_field_method_get("is_search", |_, me| Ok(me.is_search())); - fields.add_field_method_get("is_archive", |_, me| Ok(me.is_archive())); fields.add_field_method_get("is_absolute", |_, me| Ok(me.is_absolute())); fields.add_field_method_get("has_root", |_, me| Ok(me.has_root())); } diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index c1ea5387..547b5824 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -40,9 +40,10 @@ impl AsUrl for UrlBuf { fn as_url(&self) -> Url<'_> { match self { Self::Regular(loc) => Url::Regular(loc.as_loc()), - Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain }, - Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain }, - Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain }, + Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, + Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, + Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, + Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, } } } @@ -61,9 +62,10 @@ impl AsUrl for UrlCow<'_> { fn as_url(&self) -> Url<'_> { match self { Self::Regular(loc) => Url::Regular(loc.as_loc()), - Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain }, - Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain }, - Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain }, + Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, + Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, + Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, + Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, } } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index a17abc9a..07fdbb8c 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,18 +1,19 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}}; +use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}, sync::Arc}; use anyhow::Result; use hashbrown::Equivalent; use serde::Serialize; use super::Encode as EncodeUrl; -use crate::{loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, pool::{InternStr, Pool}, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; +use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{Encode as EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub enum Url<'a> { Regular(Loc<'a>), - Search { loc: Loc<'a>, domain: &'a str }, - Archive { loc: Loc<'a>, domain: &'a str }, - Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, domain: &'a str }, + Search { loc: Loc<'a>, auth: &'a Arc }, + Mount { loc: Loc<'a>, auth: &'a Arc }, + Scope { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc }, + Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc }, } // --- Eq @@ -31,7 +32,7 @@ impl Debug for Url<'_> { if self.is_regular() { write!(f, "{}", self.loc().display()) } else { - write!(f, "{}{}", EncodeScheme(*self), self.loc().display()) + write!(f, "{}{}", EncodeSpec(*self), self.loc().display()) } } } @@ -39,9 +40,9 @@ impl Debug for Url<'_> { impl Serialize for Url<'_> { fn serialize(&self, serializer: S) -> Result { let (kind, loc) = (self.kind(), self.loc()); - match (kind == SchemeKind::Regular, loc.to_str()) { + match (kind == AuthKind::Regular, loc.to_str()) { (true, Ok(s)) => serializer.serialize_str(s), - (false, Ok(s)) => serializer.serialize_str(&format!("{}{s}", EncodeScheme(*self))), + (false, Ok(s)) => serializer.serialize_str(&format!("{}{s}", EncodeSpec(*self))), (_, Err(_)) => serializer.collect_str(&EncodeUrl(*self)), } } @@ -53,6 +54,17 @@ impl<'a> Url<'a> { self.loc().as_os().ok().filter(|_| self.kind().is_local()) } + #[inline] + pub fn auth(self) -> &'a Auth { + match self { + Self::Regular(_) => &Auth::DEFAULT, + Self::Search { auth, .. } + | Self::Mount { auth, .. } + | Self::Scope { auth, .. } + | Self::Sftp { auth, .. } => auth, + } + } + #[inline] pub fn as_regular(self) -> Result { Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) @@ -61,9 +73,10 @@ impl<'a> Url<'a> { pub fn base(self) -> Self { match self { Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), - Self::Search { loc, domain } => Self::Search { loc: Loc::zeroed(loc.base()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::zeroed(loc.base()), domain }, - Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, + Self::Search { loc, auth } => Self::Search { loc: Loc::zeroed(loc.base()), auth }, + Self::Mount { loc, auth } => Self::Mount { loc: Loc::zeroed(loc.base()), auth }, + Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.base()), auth }, + Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.base()), auth }, } } @@ -73,7 +86,7 @@ impl<'a> Url<'a> { #[inline] pub fn covariant(self, other: impl AsUrl) -> bool { let other = other.as_url(); - self.loc() == other.loc() && self.scheme().covariant(other.scheme()) + self.loc() == other.loc() && self.auth().covariant(other.auth()) } #[inline] @@ -81,7 +94,8 @@ impl<'a> Url<'a> { Some(match self { Self::Regular(loc) => loc.extension()?.as_strand(), Self::Search { loc, .. } => loc.extension()?.as_strand(), - Self::Archive { loc, .. } => loc.extension()?.as_strand(), + Self::Mount { loc, .. } => loc.extension()?.as_strand(), + Self::Scope { loc, .. } => loc.extension()?.as_strand(), Self::Sftp { loc, .. } => loc.extension()?.as_strand(), }) } @@ -91,7 +105,8 @@ impl<'a> Url<'a> { match self { Self::Regular(loc) => loc.has_base(), Self::Search { loc, .. } => loc.has_base(), - Self::Archive { loc, .. } => loc.has_base(), + Self::Mount { loc, .. } => loc.has_base(), + Self::Scope { loc, .. } => loc.has_base(), Self::Sftp { loc, .. } => loc.has_base(), } } @@ -104,7 +119,8 @@ impl<'a> Url<'a> { match self { Self::Regular(loc) => loc.has_trail(), Self::Search { loc, .. } => loc.has_trail(), - Self::Archive { loc, .. } => loc.has_trail(), + Self::Mount { loc, .. } => loc.has_trail(), + Self::Scope { loc, .. } => loc.has_trail(), Self::Sftp { loc, .. } => loc.has_trail(), } } @@ -112,18 +128,6 @@ impl<'a> Url<'a> { #[inline] pub fn is_absolute(self) -> bool { self.loc().is_absolute() } - #[inline] - pub fn is_archive(self) -> bool { matches!(self, Self::Archive { .. }) } - - #[inline] - pub fn is_internal(self) -> bool { - match self { - Self::Regular(_) | Self::Sftp { .. } => true, - Self::Search { .. } => !self.uri().is_empty(), - Self::Archive { .. } => false, - } - } - #[inline] pub fn is_regular(self) -> bool { matches!(self, Self::Regular(_)) } @@ -131,21 +135,15 @@ impl<'a> Url<'a> { pub fn is_search(self) -> bool { matches!(self, Self::Search { .. }) } #[inline] - pub fn kind(self) -> SchemeKind { - match self { - Self::Regular(_) => SchemeKind::Regular, - Self::Search { .. } => SchemeKind::Search, - Self::Archive { .. } => SchemeKind::Archive, - Self::Sftp { .. } => SchemeKind::Sftp, - } - } + pub fn kind(self) -> AuthKind { self.auth().kind } #[inline] pub fn loc(self) -> PathDyn<'a> { match self { Self::Regular(loc) => loc.as_path(), Self::Search { loc, .. } => loc.as_path(), - Self::Archive { loc, .. } => loc.as_path(), + Self::Mount { loc, .. } => loc.as_path(), + Self::Scope { loc, .. } => loc.as_path(), Self::Sftp { loc, .. } => loc.as_path(), } } @@ -155,7 +153,8 @@ impl<'a> Url<'a> { Some(match self { Self::Regular(loc) => loc.file_name()?.as_strand(), Self::Search { loc, .. } => loc.file_name()?.as_strand(), - Self::Archive { loc, .. } => loc.file_name()?.as_strand(), + Self::Mount { loc, .. } => loc.file_name()?.as_strand(), + Self::Scope { loc, .. } => loc.file_name()?.as_strand(), Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), }) } @@ -175,21 +174,24 @@ impl<'a> Url<'a> { // Search Self::Search { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), - Self::Search { loc, domain } => { - Self::Search { loc: Loc::new(loc.parent()?, loc.base(), loc.base()), domain } + Self::Search { loc, auth } => { + Self::Search { loc: Loc::new(loc.parent()?, loc.base(), loc.base()), auth } } - // Archive - Self::Archive { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), - Self::Archive { loc, domain } if uri.components().nth(1).is_none() => { - Self::Archive { loc: Loc::zeroed(loc.parent()?), domain } + // Mount + Self::Mount { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Mount { loc, auth } if uri.components().nth(1).is_none() => { + Self::Mount { loc: Loc::zeroed(loc.parent()?), auth } } - Self::Archive { loc, domain } => { - Self::Archive { loc: Loc::floated(loc.parent()?, loc.base()), domain } + Self::Mount { loc, auth } => { + Self::Mount { loc: Loc::floated(loc.parent()?, loc.base()), auth } } + // Scope + Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.parent()?), auth }, + // SFTP - Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.parent()?), domain }, + Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.parent()?), auth }, }) } @@ -198,15 +200,17 @@ impl<'a> Url<'a> { Self::Regular(Loc::bare(path.as_ref())) } - #[inline] - pub fn scheme(self) -> SchemeRef<'a> { - let (uri, urn) = SchemeCow::retrieve_ports(self); - match self { - Self::Regular(_) => SchemeRef::Regular { uri, urn }, - Self::Search { domain, .. } => SchemeRef::Search { domain, uri, urn }, - Self::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn }, - Self::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn }, - } + pub fn spec(self) -> Spec { + let auth = match self { + Self::Regular(_) => Auth::default_arc(), + Self::Search { auth, .. } + | Self::Mount { auth, .. } + | Self::Scope { auth, .. } + | Self::Sftp { auth, .. } => auth.clone(), + }; + + let (uri, urn) = Spec::retrieve_ports(self); + Spec { auth, uri, urn } } #[inline] @@ -214,7 +218,8 @@ impl<'a> Url<'a> { Some(match self { Self::Regular(loc) => loc.file_stem()?.as_strand(), Self::Search { loc, .. } => loc.file_stem()?.as_strand(), - Self::Archive { loc, .. } => loc.file_stem()?.as_strand(), + Self::Mount { loc, .. } => loc.file_stem()?.as_strand(), + Self::Scope { loc, .. } => loc.file_stem()?.as_strand(), Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), }) } @@ -222,10 +227,10 @@ impl<'a> Url<'a> { #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } - pub fn to_search(self, domain: impl AsRef) -> Result { + pub fn to_search(self, query: impl AsRef) -> Result { Ok(UrlBuf::Search { - loc: LocBuf::::zeroed(self.loc().to_os_owned()?), - domain: Pool::::intern(domain), + loc: LocBuf::::zeroed(self.loc().to_os_owned()?), + auth: Auth::search(query.as_ref()), }) } @@ -234,31 +239,33 @@ impl<'a> Url<'a> { match self { Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())), - Self::Search { loc, domain } if uri.is_empty() => { - Self::Search { loc: Loc::zeroed(loc.trail()), domain } + Self::Search { loc, auth } if uri.is_empty() => { + Self::Search { loc: Loc::zeroed(loc.trail()), auth } } - Self::Search { loc, domain } => { - Self::Search { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain } + Self::Search { loc, auth } => { + Self::Search { loc: Loc::new(loc.trail(), loc.base(), loc.base()), auth } } - Self::Archive { loc, domain } if uri.is_empty() => { - Self::Archive { loc: Loc::zeroed(loc.trail()), domain } + Self::Mount { loc, auth } if uri.is_empty() => { + Self::Mount { loc: Loc::zeroed(loc.trail()), auth } } - Self::Archive { loc, domain } => { - Self::Archive { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain } + Self::Mount { loc, auth } => { + Self::Mount { loc: Loc::new(loc.trail(), loc.base(), loc.base()), auth } } - Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, + Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.trail()), auth }, + + Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.trail()), auth }, } } pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { match self { - Self::Regular(loc) | Self::Search { loc, .. } | Self::Archive { loc, .. } => { + Self::Regular(loc) | Self::Search { loc, .. } | Self::Mount { loc, .. } => { let (base, rest, urn) = loc.triple(); (base.as_path(), rest.as_path(), urn.as_path()) } - Self::Sftp { loc, .. } => { + Self::Scope { loc, .. } | Self::Sftp { loc, .. } => { let (base, rest, urn) = loc.triple(); (base.as_path(), rest.as_path(), urn.as_path()) } @@ -268,7 +275,7 @@ impl<'a> Url<'a> { #[inline] pub fn try_ends_with(self, child: impl AsUrl) -> Result { let child = child.as_url(); - Ok(self.loc().try_ends_with(child.loc())? && self.scheme().covariant(child.scheme())) + Ok(self.loc().try_ends_with(child.loc())? && self.auth().covariant(child.auth())) } pub fn try_join(self, path: impl AsStrand) -> Result { @@ -277,26 +284,28 @@ impl<'a> Url<'a> { Ok(match self { Self::Regular(_) => UrlBuf::Regular(joined.into_os()?.into()), - Self::Search { loc, domain } if joined.try_starts_with(loc.base())? => UrlBuf::Search { - loc: LocBuf::::new(joined.try_into()?, loc.base(), loc.base()), - domain: domain.intern(), - }, - Self::Search { domain, .. } => UrlBuf::Search { - loc: LocBuf::::zeroed(joined.into_os()?), - domain: domain.intern(), + Self::Search { loc, auth } if joined.try_starts_with(loc.base())? => UrlBuf::Search { + loc: LocBuf::::new(joined.try_into()?, loc.base(), loc.base()), + auth: auth.clone(), }, + Self::Search { auth, .. } => { + UrlBuf::Search { loc: LocBuf::::zeroed(joined.into_os()?), auth: auth.clone() } + } - Self::Archive { loc, domain } if joined.try_starts_with(loc.base())? => UrlBuf::Archive { - loc: LocBuf::::floated(joined.try_into()?, loc.base()), - domain: domain.intern(), - }, - Self::Archive { domain, .. } => UrlBuf::Archive { - loc: LocBuf::::zeroed(joined.into_os()?), - domain: domain.intern(), + Self::Mount { loc, auth } if joined.try_starts_with(loc.base())? => UrlBuf::Mount { + loc: LocBuf::::floated(joined.try_into()?, loc.base()), + auth: auth.clone(), }, + Self::Mount { auth, .. } => { + UrlBuf::Mount { loc: LocBuf::::zeroed(joined.into_os()?), auth: auth.clone() } + } - Self::Sftp { domain, .. } => { - UrlBuf::Sftp { loc: joined.into_unix()?.into(), domain: domain.intern() } + Self::Scope { auth, .. } => { + UrlBuf::Scope { loc: joined.into_unix()?.into(), auth: auth.clone() } + } + + Self::Sftp { auth, .. } => { + UrlBuf::Sftp { loc: joined.into_unix()?.into(), auth: auth.clone() } } }) } @@ -309,7 +318,7 @@ impl<'a> Url<'a> { let b = rep.encoded_bytes(); if take == 0 { return UrlCow::try_from(b); - } else if SchemeKind::parse(b)?.is_some() { + } else if ParsedSpec::parse(b)?.has_scheme() { return UrlCow::try_from(b); } @@ -320,30 +329,38 @@ impl<'a> Url<'a> { let url = match self { Self::Regular(_) => UrlBuf::from(path.into_os()?), - Self::Search { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Search { - loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), - domain: domain.intern(), + Self::Search { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Search { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + auth: auth.clone(), }, - Self::Archive { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Archive { - loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), - domain: domain.intern(), + Self::Mount { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Mount { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + auth: auth.clone(), }, - Self::Sftp { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Sftp { - loc: LocBuf::::new(path.into_unix()?, loc.base(), loc.trail()), - domain: domain.intern(), + Self::Scope { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Scope { + loc: LocBuf::::new(path.into_unix()?, loc.base(), loc.trail()), + auth: auth.clone(), + }, + Self::Sftp { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Sftp { + loc: LocBuf::::new(path.into_unix()?, loc.base(), loc.trail()), + auth: auth.clone(), }, - Self::Search { domain, .. } => UrlBuf::Search { - loc: LocBuf::::saturated(path.into_os()?, self.kind()), - domain: domain.intern(), + Self::Search { auth, .. } => UrlBuf::Search { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + auth: auth.clone(), }, - Self::Archive { domain, .. } => UrlBuf::Archive { - loc: LocBuf::::saturated(path.into_os()?, self.kind()), - domain: domain.intern(), + Self::Mount { auth, .. } => UrlBuf::Mount { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + auth: auth.clone(), }, - Self::Sftp { domain, .. } => UrlBuf::Sftp { - loc: LocBuf::::saturated(path.into_unix()?, self.kind()), - domain: domain.intern(), + Self::Scope { auth, .. } => UrlBuf::Scope { + loc: LocBuf::::saturated(path.into_unix()?, self.kind()), + auth: auth.clone(), + }, + Self::Sftp { auth, .. } => UrlBuf::Sftp { + loc: LocBuf::::saturated(path.into_unix()?, self.kind()), + auth: auth.clone(), }, }; @@ -353,7 +370,7 @@ impl<'a> Url<'a> { #[inline] pub fn try_starts_with(self, base: impl AsUrl) -> Result { let base = base.as_url(); - Ok(self.loc().try_starts_with(base.loc())? && self.scheme().covariant(base.scheme())) + Ok(self.loc().try_starts_with(base.loc())? && self.auth().covariant(base.auth())) } pub fn try_strip_prefix(self, base: impl AsUrl) -> Result, StripPrefixError> { @@ -362,43 +379,19 @@ impl<'a> Url<'a> { let base = base.as_url(); let prefix = self.loc().try_strip_prefix(base.loc())?; + if self.auth().covariant(base.auth()) { + return Ok(prefix); + } match (self, base) { - // Same scheme - (U::Regular(_), U::Regular(_)) => Ok(prefix), - (U::Search { .. }, U::Search { .. }) => Ok(prefix), - (U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { - (a == b).then_some(prefix).ok_or(Exotic) - } - (U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => { - (a == b).then_some(prefix).ok_or(Exotic) - } - - // Both are local files - (U::Regular(_), U::Search { .. }) => Ok(prefix), - (U::Search { .. }, U::Regular(_)) => Ok(prefix), - - // Only the entry of archives is a local file - (U::Regular(_), U::Archive { .. }) => { + // A mount portal is the local source file until it gains an inner URI. + (U::Regular(_) | U::Search { .. }, U::Mount { .. }) => { base.uri().is_empty().then_some(prefix).ok_or(NotPrefix) } - (U::Search { .. }, U::Archive { .. }) => { - base.uri().is_empty().then_some(prefix).ok_or(NotPrefix) - } - (U::Archive { .. }, U::Regular(_)) => { + (U::Mount { .. }, U::Regular(_) | U::Search { .. }) => { self.uri().is_empty().then_some(prefix).ok_or(NotPrefix) } - (U::Archive { .. }, U::Search { .. }) => { - self.uri().is_empty().then_some(prefix).ok_or(NotPrefix) - } - - // Independent virtual file space - (U::Regular(_), U::Sftp { .. }) => Err(Exotic), - (U::Search { .. }, U::Sftp { .. }) => Err(Exotic), - (U::Archive { .. }, U::Sftp { .. }) => Err(Exotic), - (U::Sftp { .. }, U::Regular(_)) => Err(Exotic), - (U::Sftp { .. }, U::Search { .. }) => Err(Exotic), - (U::Sftp { .. }, U::Archive { .. }) => Err(Exotic), + _ => Err(Exotic), } } @@ -408,43 +401,19 @@ impl<'a> Url<'a> { let other = other.as_url(); let suffix = self.loc().try_strip_suffix(other.loc())?; + if self.auth().covariant(other.auth()) { + return Ok(suffix); + } match (self, other) { - // Same scheme - (U::Regular(_), U::Regular(_)) => Ok(suffix), - (U::Search { .. }, U::Search { .. }) => Ok(suffix), - (U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { - (a == b).then_some(suffix).ok_or(Exotic) - } - (U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => { - (a == b).then_some(suffix).ok_or(Exotic) - } - - // Both are local files - (U::Regular(_), U::Search { .. }) => Ok(suffix), - (U::Search { .. }, U::Regular(_)) => Ok(suffix), - - // Only the entry of archives is a local file - (U::Regular(_), U::Archive { .. }) => { + // A mount portal is the local source file until it gains an inner URI. + (U::Regular(_) | U::Search { .. }, U::Mount { .. }) => { other.uri().is_empty().then_some(suffix).ok_or(NotSuffix) } - (U::Search { .. }, U::Archive { .. }) => { - other.uri().is_empty().then_some(suffix).ok_or(NotSuffix) - } - (U::Archive { .. }, U::Regular(_)) => { + (U::Mount { .. }, U::Regular(_) | U::Search { .. }) => { self.uri().is_empty().then_some(suffix).ok_or(NotSuffix) } - (U::Archive { .. }, U::Search { .. }) => { - self.uri().is_empty().then_some(suffix).ok_or(NotSuffix) - } - - // Independent virtual file space - (U::Regular(_), U::Sftp { .. }) => Err(Exotic), - (U::Search { .. }, U::Sftp { .. }) => Err(Exotic), - (U::Archive { .. }, U::Sftp { .. }) => Err(Exotic), - (U::Sftp { .. }, U::Regular(_)) => Err(Exotic), - (U::Sftp { .. }, U::Search { .. }) => Err(Exotic), - (U::Sftp { .. }, U::Archive { .. }) => Err(Exotic), + _ => Err(Exotic), } } @@ -453,7 +422,8 @@ impl<'a> Url<'a> { match self { Self::Regular(loc) => loc.uri().as_path(), Self::Search { loc, .. } => loc.uri().as_path(), - Self::Archive { loc, .. } => loc.uri().as_path(), + Self::Mount { loc, .. } => loc.uri().as_path(), + Self::Scope { loc, .. } => loc.uri().as_path(), Self::Sftp { loc, .. } => loc.uri().as_path(), } } @@ -463,7 +433,8 @@ impl<'a> Url<'a> { match self { Self::Regular(loc) => loc.urn().as_path(), Self::Search { loc, .. } => loc.urn().as_path(), - Self::Archive { loc, .. } => loc.urn().as_path(), + Self::Mount { loc, .. } => loc.urn().as_path(), + Self::Scope { loc, .. } => loc.urn().as_path(), Self::Sftp { loc, .. } => loc.urn().as_path(), } } diff --git a/yazi-shim/src/fs/error.rs b/yazi-shim/src/fs/error.rs index 55e7796a..40eb0e94 100644 --- a/yazi-shim/src/fs/error.rs +++ b/yazi-shim/src/fs/error.rs @@ -31,6 +31,16 @@ impl From for Error { fn from(kind: io::ErrorKind) -> Self { Self::Kind(kind) } } +impl From for io::Error { + fn from(value: Error) -> Self { + match value { + Error::Kind(kind) => Self::from(kind), + Error::Raw(code) => Self::from_raw_os_error(code), + Error::Custom { kind, message, .. } => Self::new(kind, message.to_string()), + } + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/yazi-vfs/Cargo.toml b/yazi-vfs/Cargo.toml index 4bbbe40c..c36d1a4b 100644 --- a/yazi-vfs/Cargo.toml +++ b/yazi-vfs/Cargo.toml @@ -14,9 +14,10 @@ workspace = true [dependencies] yazi-binding = { path = "../yazi-binding", version = "26.5.6" } -yazi-codegen = { path = "../yazi-codegen", version = "26.5.6" } +yazi-config = { path = "../yazi-config", version = "26.5.6" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } +yazi-runner = { path = "../yazi-runner", version = "26.5.6" } yazi-sftp = { path = "../yazi-sftp", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } @@ -31,8 +32,6 @@ hashbrown = { workspace = true } mlua = { workspace = true } parking_lot = { workspace = true } russh = { workspace = true } -serde = { workspace = true } tokio = { workspace = true } -toml = { workspace = true } tracing = { workspace = true } typed-path = { workspace = true } diff --git a/yazi-vfs/src/cha.rs b/yazi-vfs/src/cha.rs index d93dcb8c..639b430d 100644 --- a/yazi-vfs/src/cha.rs +++ b/yazi-vfs/src/cha.rs @@ -3,7 +3,7 @@ use std::io; use yazi_fs::cha::{Cha, ChaKind}; use yazi_shared::url::AsUrl; -use crate::provider; +use crate::engine; pub trait VfsCha: Sized { fn from_url(url: impl AsUrl) -> impl Future>; @@ -17,7 +17,7 @@ impl VfsCha for Cha { #[inline] async fn from_url(url: impl AsUrl) -> io::Result { let url = url.as_url(); - Ok(Self::from_follow(url, provider::symlink_metadata(url).await?).await) + Ok(Self::from_follow(url, engine::symlink_metadata(url).await?).await) } async fn from_follow(url: U, mut cha: Self) -> Self @@ -29,7 +29,7 @@ impl VfsCha for Cha { if cha.is_link() { retain |= ChaKind::FOLLOW; - cha = provider::metadata(url).await.unwrap_or(cha); + cha = engine::metadata(url).await.unwrap_or(cha); } cha.attach(retain) diff --git a/yazi-vfs/src/config/mod.rs b/yazi-vfs/src/config/mod.rs deleted file mode 100644 index e35a3ea7..00000000 --- a/yazi-vfs/src/config/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(service services sftp vfs); diff --git a/yazi-vfs/src/config/service.rs b/yazi-vfs/src/config/service.rs deleted file mode 100644 index 283f7db7..00000000 --- a/yazi-vfs/src/config/service.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::config::ServiceSftp; - -#[derive(Deserialize, Serialize)] -#[serde(tag = "type", rename_all = "kebab-case")] -pub enum Service { - Sftp(ServiceSftp), -} - -impl TryFrom<&'static Service> for &'static ServiceSftp { - type Error = &'static str; - - fn try_from(value: &'static Service) -> Result { - match value { - Service::Sftp(p) => Ok(p), - } - } -} diff --git a/yazi-vfs/src/config/services.rs b/yazi-vfs/src/config/services.rs deleted file mode 100644 index 1264c0ff..00000000 --- a/yazi-vfs/src/config/services.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::ops::Deref; - -use anyhow::Result; -use hashbrown::HashMap; -use serde::{Deserialize, Deserializer, Serialize, de, de::{MapAccess, Visitor}}; -use yazi_codegen::DeserializeOver; -use yazi_shim::toml::DeserializeOverWith; - -use crate::config::Service; - -#[derive(Serialize, Deserialize, DeserializeOver)] -pub struct Services(HashMap); - -impl Deref for Services { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DeserializeOverWith for Services { - fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result { - struct V(Services); - - impl<'de> Visitor<'de> for V { - type Value = Services; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("a map of VFS services") - } - - fn visit_map>(mut self, mut map: M) -> Result { - while let Some(key) = map.next_key::()? { - if key.is_empty() || key.len() > 20 { - return Err(de::Error::custom(format!( - "VFS name `{key}` must be between 1 and 20 characters" - ))); - } else if !key.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { - return Err(de::Error::custom(format!("VFS name `{key}` must be in kebab-case"))); - } - self.0.0.insert(key, map.next_value()?); - } - Ok(self.0) - } - } - - de.deserialize_map(V(self)) - } -} diff --git a/yazi-vfs/src/config/vfs.rs b/yazi-vfs/src/config/vfs.rs deleted file mode 100644 index 2b90389b..00000000 --- a/yazi-vfs/src/config/vfs.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::io; - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use tokio::sync::OnceCell; -use yazi_codegen::{DeserializeOver, DeserializeOver1}; -use yazi_fs::{Xdg, ok_or_not_found}; -use yazi_shim::toml::DeserializeOver; - -use super::Service; -use crate::config::Services; - -#[derive(Deserialize, Serialize, DeserializeOver, DeserializeOver1)] -pub struct Vfs { - pub services: Services, -} - -impl Vfs { - pub async fn load() -> io::Result<&'static Self> { - pub static LOADED: OnceCell = OnceCell::const_new(); - - async fn init() -> io::Result { - tokio::task::spawn_blocking(|| -> anyhow::Result { - let vfs: Vfs = toml::from_str(&yazi_macro::config_preset!("vfs"))?; - Ok(vfs.deserialize_over(&Vfs::read()?)?) - }) - .await? - .map_err(io::Error::other) - } - - LOADED.get_or_try_init(init).await - } - - pub async fn service<'a, P>(name: &str) -> io::Result<(&'a str, P)> - where - P: TryFrom<&'a Service, Error = &'static str>, - { - let Some((key, value)) = Self::load().await?.services.get_key_value(name) else { - return Err(io::Error::other(format!("No such VFS service: {name}"))); - }; - match value.try_into() { - Ok(p) => Ok((key.as_str(), p)), - Err(e) => Err(io::Error::other(format!("VFS service `{key}` has wrong type: {e}"))), - } - } - - fn read() -> Result { - let p = Xdg::config_dir().join("vfs.toml"); - ok_or_not_found(std::fs::read_to_string(&p)) - .with_context(|| format!("Failed to read config {p:?}")) - } -} diff --git a/yazi-vfs/src/provider/sftp/absolute.rs b/yazi-vfs/src/engine/absolute.rs similarity index 53% rename from yazi-vfs/src/provider/sftp/absolute.rs rename to yazi-vfs/src/engine/absolute.rs index 567d804b..53515425 100644 --- a/yazi-vfs/src/provider/sftp/absolute.rs +++ b/yazi-vfs/src/engine/absolute.rs @@ -1,18 +1,15 @@ use yazi_fs::CWD; use yazi_shared::url::{UrlCow, UrlLike}; -pub fn try_absolute<'a, U>(url: U) -> Option> +pub(crate) fn try_absolute_impl<'a, U>(url: U) -> Option> where U: Into>, { - try_absolute_impl(url.into()) -} - -fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option> { + let url = url.into(); if url.is_absolute() { Some(url) } else if let cwd = CWD.load() - && cwd.scheme().covariant(url.scheme()) + && cwd.auth().covariant(url.auth()) { Some(cwd.try_join(url.loc()).ok()?.into()) } else { diff --git a/yazi-vfs/src/provider/calculator.rs b/yazi-vfs/src/engine/calculator.rs similarity index 96% rename from yazi-vfs/src/provider/calculator.rs rename to yazi-vfs/src/engine/calculator.rs index e3aba5d0..bf5cfa5d 100644 --- a/yazi-vfs/src/provider/calculator.rs +++ b/yazi-vfs/src/engine/calculator.rs @@ -1,7 +1,7 @@ use std::{collections::VecDeque, io, time::{Duration, Instant}}; use either::Either; -use yazi_fs::{cha::Cha, provider::{DirReader, FileHolder}}; +use yazi_fs::{cha::Cha, engine::{DirReader, FileHolder}}; use yazi_shared::url::{AsUrl, UrlBuf}; use super::ReadDir; diff --git a/yazi-vfs/src/provider/copier.rs b/yazi-vfs/src/engine/copier.rs similarity index 90% rename from yazi-vfs/src/provider/copier.rs rename to yazi-vfs/src/engine/copier.rs index 358c1b64..1b952fa7 100644 --- a/yazi-vfs/src/provider/copier.rs +++ b/yazi-vfs/src/engine/copier.rs @@ -2,17 +2,17 @@ use std::{io::{self, SeekFrom}, sync::{Arc, atomic::{AtomicU64, Ordering}}}; use futures::{StreamExt, TryStreamExt}; use tokio::{io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader, BufWriter}, select, sync::{mpsc, oneshot}}; -use yazi_fs::{cha::Cha, provider::{Attrs, FileBuilder}}; +use yazi_fs::{cha::Cha, engine::{Attrs, FileBuilder}}; use yazi_shared::url::{Url, UrlBuf}; -use crate::provider::{self, Gate, RwFile}; +use crate::engine::{self, Demand, RwFile}; const BUF_SIZE: usize = 512 * 1024; const PER_CHUNK: u64 = 8 * 1024 * 1024; pub(super) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: Attrs) -> io::Result { - let src = provider::open(from).await?; - let dist = provider::create(to).await?; + let src = engine::open(from).await?; + let dist = engine::create(to).await?; let mut reader = BufReader::with_capacity(BUF_SIZE, src); let mut writer = BufWriter::with_capacity(BUF_SIZE, dist); @@ -24,7 +24,7 @@ pub(super) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: Attrs) -> io::R Ok(written) } -pub(super) fn copy_with_progress_impl( +pub(super) fn copy_progressive_impl( from: UrlBuf, to: UrlBuf, attrs: Attrs, @@ -65,10 +65,10 @@ impl ProgressiveCopier { } async fn init(&self) -> io::Result<(Cha, RwFile, RwFile)> { - let src = provider::open(&self.from).await?; + let src = engine::open(&self.from).await?; let cha = src.metadata().await?; - let dist = provider::create(&self.to).await?; + let dist = engine::create(&self.to).await?; dist.set_len(cha.len).await?; Ok((cha, src, dist)) } @@ -122,11 +122,11 @@ impl ProgressiveCopier { let mut src = BufReader::with_capacity(BUF_SIZE, match src { Some(f) => f, - None => provider::open(&self.from).await?, + None => engine::open(&self.from).await?, }); let mut dist = BufWriter::with_capacity(BUF_SIZE, match dist { Some(f) => f, - None => Gate::default().write(true).open(&self.to).await?, + None => Demand::default().write(true).open(&self.to).await?, }); src.seek(SeekFrom::Start(offset)).await?; diff --git a/yazi-vfs/src/provider/gate.rs b/yazi-vfs/src/engine/demand.rs similarity index 59% rename from yazi-vfs/src/provider/gate.rs rename to yazi-vfs/src/engine/demand.rs index 3210dd16..ecbbf140 100644 --- a/yazi-vfs/src/provider/gate.rs +++ b/yazi-vfs/src/engine/demand.rs @@ -2,40 +2,32 @@ use std::io; use mlua::{AnyUserData, IntoLuaMulti, UserData, UserDataMethods, Value}; use yazi_binding::Error; -use yazi_fs::provider::{Attrs, FileBuilder}; -use yazi_shared::{scheme::SchemeKind, url::{AsUrl, UrlRef}}; +use yazi_fs::engine::{Attrs, FileBuilder}; +use yazi_shared::{auth::AuthKind, url::{AsUrl, UrlRef}}; #[derive(Clone, Copy, Default)] -pub struct Gate { - pub(super) append: bool, - pub(super) attrs: Attrs, - pub(super) create: bool, - pub(super) create_new: bool, - pub(super) read: bool, - pub(super) truncate: bool, - pub(super) write: bool, -} +pub struct Demand(yazi_fs::engine::Demand); -impl FileBuilder for Gate { +impl FileBuilder for Demand { type File = super::RwFile; fn append(&mut self, append: bool) -> &mut Self { - self.append = append; + self.0.append = append; self } fn attrs(&mut self, attrs: Attrs) -> &mut Self { - self.attrs = attrs; + self.0.attrs = attrs; self } fn create(&mut self, create: bool) -> &mut Self { - self.create = create; + self.0.create = create; self } fn create_new(&mut self, create_new: bool) -> &mut Self { - self.create_new = create_new; + self.0.create_new = create_new; self } @@ -45,59 +37,33 @@ impl FileBuilder for Gate { { let url = url.as_url(); Ok(match url.kind() { - SchemeKind::Regular | SchemeKind::Search => { - self.build::().open(url).await?.into() + AuthKind::Regular | AuthKind::Search => { + self.0.build::().open(url).await?.into() } - SchemeKind::Archive => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? + AuthKind::Mount | AuthKind::Scope => { + self.0.build::().open(url).await?.into() } - SchemeKind::Sftp => self.build::().open(url).await?.into(), + AuthKind::Sftp => self.0.build::().open(url).await?.into(), }) } fn read(&mut self, read: bool) -> &mut Self { - self.read = read; + self.0.read = read; self } fn truncate(&mut self, truncate: bool) -> &mut Self { - self.truncate = truncate; + self.0.truncate = truncate; self } fn write(&mut self, write: bool) -> &mut Self { - self.write = write; + self.0.write = write; self } } -impl Gate { - fn build(self) -> T { - let mut gate = T::default(); - if self.append { - gate.append(true); - } - gate.attrs(self.attrs); - if self.create { - gate.create(true); - } - if self.create_new { - gate.create_new(true); - } - if self.read { - gate.read(true); - } - if self.truncate { - gate.truncate(true); - } - if self.write { - gate.write(true); - } - gate - } -} - -impl UserData for Gate { +impl UserData for Demand { fn add_methods>(methods: &mut M) { methods.add_function("append", |_, (ud, append): (AnyUserData, bool)| { ud.borrow_mut::()?.append(append); diff --git a/yazi-vfs/src/provider/dir_entry.rs b/yazi-vfs/src/engine/dir_entry.rs similarity index 73% rename from yazi-vfs/src/provider/dir_entry.rs rename to yazi-vfs/src/engine/dir_entry.rs index 40b75e64..2a8b8a86 100644 --- a/yazi-vfs/src/provider/dir_entry.rs +++ b/yazi-vfs/src/engine/dir_entry.rs @@ -1,10 +1,11 @@ use std::io; -use yazi_fs::{cha::{Cha, ChaType}, provider::FileHolder}; +use yazi_fs::{cha::{Cha, ChaType}, engine::FileHolder}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::UrlBuf}; pub enum DirEntry { - Local(yazi_fs::provider::local::DirEntry), + Local(yazi_fs::engine::local::DirEntry), + Lua(super::lua::DirEntry), Sftp(super::sftp::DirEntry), } @@ -12,6 +13,7 @@ impl FileHolder for DirEntry { async fn file_type(&self) -> io::Result { match self { Self::Local(entry) => entry.file_type().await, + Self::Lua(entry) => entry.file_type().await, Self::Sftp(entry) => entry.file_type().await, } } @@ -19,6 +21,7 @@ impl FileHolder for DirEntry { async fn metadata(&self) -> io::Result { match self { Self::Local(entry) => entry.metadata().await, + Self::Lua(entry) => entry.metadata().await, Self::Sftp(entry) => entry.metadata().await, } } @@ -26,6 +29,7 @@ impl FileHolder for DirEntry { fn name(&self) -> StrandCow<'_> { match self { Self::Local(entry) => entry.name(), + Self::Lua(entry) => entry.name(), Self::Sftp(entry) => entry.name(), } } @@ -33,6 +37,7 @@ impl FileHolder for DirEntry { fn path(&self) -> PathBufDyn { match self { Self::Local(entry) => entry.path(), + Self::Lua(entry) => entry.path(), Self::Sftp(entry) => entry.path(), } } @@ -40,6 +45,7 @@ impl FileHolder for DirEntry { fn url(&self) -> UrlBuf { match self { Self::Local(entry) => entry.url(), + Self::Lua(entry) => entry.url(), Self::Sftp(entry) => entry.url(), } } diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/engine/engine.rs similarity index 59% rename from yazi-vfs/src/provider/provider.rs rename to yazi-vfs/src/engine/engine.rs index 3535bcb0..9a9ce8e9 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/engine/engine.rs @@ -1,16 +1,16 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider, local::Local}}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine, local::Local}}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; -use super::{Providers, ReadDir, RwFile}; +use super::{Engines, ReadDir, RwFile}; pub async fn absolute<'a, U>(url: &'a U) -> io::Result> where U: AsUrl, { - Providers::new(url.as_url()).await?.absolute().await + Engines::new(url.as_url()).await?.absolute().await } pub async fn calculate(url: U) -> io::Result @@ -19,7 +19,7 @@ where { let url = url.as_url(); if let Some(path) = url.as_local() { - yazi_fs::provider::local::SizeCalculator::total(path).await + yazi_fs::engine::local::SizeCalculator::total(path).await } else { super::SizeCalculator::total(url).await } @@ -29,21 +29,21 @@ pub async fn canonicalize(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.canonicalize().await + Engines::new(url.as_url()).await?.canonicalize().await } pub async fn capabilities(url: U) -> io::Result where U: AsUrl, { - Ok(Providers::new(url.as_url()).await?.capabilities()) + Engines::new(url.as_url()).await?.capabilities().await } pub async fn casefold(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.casefold().await + Engines::new(url.as_url()).await?.casefold().await } pub async fn copy(from: U, to: V, attrs: Attrs) -> io::Result @@ -55,14 +55,14 @@ where match (from.kind().is_local(), to.kind().is_local()) { (true, true) => Local::new(from).await?.copy(to.loc(), attrs).await, - (false, false) if from.scheme().covariant(to.scheme()) => { - Providers::new(from).await?.copy(to.loc(), attrs).await + (false, false) if from.auth().covariant(to.auth()) => { + Engines::new(from).await?.copy(to.loc(), attrs).await } (true, false) | (false, true) | (false, false) => super::copy_impl(from, to, attrs).await, } } -pub async fn copy_with_progress( +pub async fn copy_progressive( from: U, to: V, attrs: A, @@ -73,44 +73,44 @@ where A: Into, { let (from, to) = (from.as_url(), to.as_url()); + let attrs = attrs.into(); - match (from.kind().is_local(), to.kind().is_local()) { - (true, true) => Local::new(from).await?.copy_with_progress(to.loc(), attrs), - (false, false) if from.scheme().covariant(to.scheme()) => { - Providers::new(from).await?.copy_with_progress(to.loc(), attrs) - } - (true, false) | (false, true) | (false, false) => { - Ok(super::copy_with_progress_impl(from.to_owned(), to.to_owned(), attrs.into())) + if from.auth().covariant(to.auth()) { + let engine = Engines::new(from).await?; + if engine.capabilities().await?.copy_progressive { + return engine.copy_progressive(to.loc(), attrs); } } + + Ok(super::copy_progressive_impl(from.to_owned(), to.to_owned(), attrs)) } pub async fn create(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.create().await + Engines::new(url.as_url()).await?.create().await } pub async fn create_dir(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.create_dir().await + Engines::new(url.as_url()).await?.create_dir().await } pub async fn create_dir_all(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.create_dir_all().await + Engines::new(url.as_url()).await?.create_dir_all().await } pub async fn create_new(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.create_new().await + Engines::new(url.as_url()).await?.create_new().await } pub async fn hard_link(original: U, link: V) -> io::Result<()> @@ -119,8 +119,8 @@ where V: AsUrl, { let (original, link) = (original.as_url(), link.as_url()); - if original.scheme().covariant(link.scheme()) { - Providers::new(original).await?.hard_link(link.loc()).await + if original.auth().covariant(link.auth()) { + Engines::new(original).await?.hard_link(link.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) } @@ -132,7 +132,7 @@ where V: AsUrl, { if let (Some(a), Some(b)) = (a.as_url().as_local(), b.as_url().as_local()) { - yazi_fs::provider::local::identical(a, b).await + yazi_fs::engine::local::identical(a, b).await } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) } @@ -142,7 +142,7 @@ pub async fn metadata(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.metadata().await + Engines::new(url.as_url()).await?.metadata().await } pub async fn must_identical(a: U, b: V) -> bool @@ -157,49 +157,49 @@ pub async fn open(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.open().await + Engines::new(url.as_url()).await?.open().await } pub async fn read_dir(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.read_dir().await + Engines::new(url.as_url()).await?.read_dir().await } pub async fn read_link(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.read_link().await + Engines::new(url.as_url()).await?.read_link().await } pub async fn remove_dir(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.remove_dir().await + Engines::new(url.as_url()).await?.remove_dir().await } pub async fn remove_dir_all(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.remove_dir_all().await + Engines::new(url.as_url()).await?.remove_dir_all().await } pub async fn remove_dir_clean(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.remove_dir_clean().await + Engines::new(url.as_url()).await?.remove_dir_clean().await } pub async fn remove_file(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.remove_file().await + Engines::new(url.as_url()).await?.remove_file().await } pub async fn rename(from: U, to: V) -> io::Result<()> @@ -208,18 +208,18 @@ where V: AsUrl, { let (from, to) = (from.as_url(), to.as_url()); - if from.scheme().covariant(to.scheme()) { - Providers::new(from).await?.rename(to.loc()).await + if from.auth().covariant(to.auth()) { + Engines::new(from).await?.rename(to.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) } } -pub async fn set_mode(url: U, mode: ChaMode) -> io::Result<()> +pub async fn set_attrs(url: U, attrs: Attrs) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.set_mode(mode).await + Engines::new(url.as_url()).await?.set_attrs(attrs).await } pub async fn symlink(link: U, original: S, is_dir: F) -> io::Result<()> @@ -228,7 +228,7 @@ where S: AsStrand, F: AsyncFnOnce() -> io::Result, { - Providers::new(link.as_url()).await?.symlink(original, is_dir).await + Engines::new(link.as_url()).await?.symlink(original, is_dir).await } pub async fn symlink_dir(link: U, original: S) -> io::Result<()> @@ -236,7 +236,7 @@ where U: AsUrl, S: AsStrand, { - Providers::new(link.as_url()).await?.symlink_dir(original).await + Engines::new(link.as_url()).await?.symlink_dir(original).await } pub async fn symlink_file(link: U, original: S) -> io::Result<()> @@ -244,21 +244,21 @@ where U: AsUrl, S: AsStrand, { - Providers::new(link.as_url()).await?.symlink_file(original).await + Engines::new(link.as_url()).await?.symlink_file(original).await } pub async fn symlink_metadata(url: U) -> io::Result where U: AsUrl, { - Providers::new(url.as_url()).await?.symlink_metadata().await + Engines::new(url.as_url()).await?.symlink_metadata().await } pub async fn trash(url: U) -> io::Result<()> where U: AsUrl, { - Providers::new(url.as_url()).await?.trash().await + Engines::new(url.as_url()).await?.trash().await } pub fn try_absolute<'a, U>(url: U) -> Option> @@ -267,9 +267,8 @@ where { let url = url.into(); match url.as_url() { - Url::Regular(_) | Url::Search { .. } => yazi_fs::provider::local::try_absolute(url), - Url::Archive { .. } => None, // TODO - Url::Sftp { .. } => crate::provider::sftp::try_absolute(url), + Url::Regular(_) | Url::Search { .. } => yazi_fs::engine::local::try_absolute(url), + Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => super::try_absolute_impl(url), } } @@ -278,5 +277,5 @@ where U: AsUrl, C: AsRef<[u8]>, { - Providers::new(url.as_url()).await?.write(contents).await + Engines::new(url.as_url()).await?.write(contents).await } diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/engine/engines.rs similarity index 68% rename from yazi-vfs/src/provider/providers.rs rename to yazi-vfs/src/engine/engines.rs index 5c1d30f5..cace26f6 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/engine/engines.rs @@ -1,25 +1,27 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider}}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}}; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] -pub(super) enum Providers<'a> { - Local(yazi_fs::provider::local::Local<'a>), +pub(super) enum Engines<'a> { + Local(yazi_fs::engine::local::Local<'a>), + Lua(super::lua::Lua<'a>), Sftp(super::sftp::Sftp<'a>), } -impl<'a> Provider for Providers<'a> { +impl<'a> Engine for Engines<'a> { + type Demand = super::Demand; type File = super::RwFile; - type Gate = super::Gate; - type Me<'b> = Providers<'b>; + type Me<'b> = Engines<'b>; type ReadDir = super::ReadDir; type UrlCow = UrlCow<'a>; async fn absolute(&self) -> io::Result { match self { Self::Local(p) => p.absolute().await, + Self::Lua(p) => p.absolute().await, Self::Sftp(p) => p.absolute().await, } } @@ -27,20 +29,23 @@ impl<'a> Provider for Providers<'a> { async fn canonicalize(&self) -> io::Result { match self { Self::Local(p) => p.canonicalize().await, + Self::Lua(p) => p.canonicalize().await, Self::Sftp(p) => p.canonicalize().await, } } - fn capabilities(&self) -> Capabilities { + async fn capabilities(&self) -> io::Result { match self { - Self::Local(p) => p.capabilities(), - Self::Sftp(p) => p.capabilities(), + Self::Local(p) => p.capabilities().await, + Self::Lua(p) => p.capabilities().await, + Self::Sftp(p) => p.capabilities().await, } } async fn casefold(&self) -> io::Result { match self { Self::Local(p) => p.casefold().await, + Self::Lua(p) => p.casefold().await, Self::Sftp(p) => p.casefold().await, } } @@ -51,24 +56,27 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.copy(to, attrs).await, + Self::Lua(p) => p.copy(to, attrs).await, Self::Sftp(p) => p.copy(to, attrs).await, } } - fn copy_with_progress(&self, to: P, attrs: A) -> io::Result>> + fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where P: AsPath, A: Into, { match self { - Self::Local(p) => p.copy_with_progress(to, attrs), - Self::Sftp(p) => p.copy_with_progress(to, attrs), + Self::Local(p) => p.copy_progressive(to, attrs), + Self::Lua(p) => p.copy_progressive(to, attrs), + Self::Sftp(p) => p.copy_progressive(to, attrs), } } async fn create(&self) -> io::Result { Ok(match self { Self::Local(p) => p.create().await?.into(), + Self::Lua(p) => p.create().await?.into(), Self::Sftp(p) => p.create().await?.into(), }) } @@ -76,6 +84,7 @@ impl<'a> Provider for Providers<'a> { async fn create_dir(&self) -> io::Result<()> { match self { Self::Local(p) => p.create_dir().await, + Self::Lua(p) => p.create_dir().await, Self::Sftp(p) => p.create_dir().await, } } @@ -83,6 +92,7 @@ impl<'a> Provider for Providers<'a> { async fn create_dir_all(&self) -> io::Result<()> { match self { Self::Local(p) => p.create_dir_all().await, + Self::Lua(p) => p.create_dir_all().await, Self::Sftp(p) => p.create_dir_all().await, } } @@ -90,6 +100,7 @@ impl<'a> Provider for Providers<'a> { async fn create_new(&self) -> io::Result { Ok(match self { Self::Local(p) => p.create_new().await?.into(), + Self::Lua(p) => p.create_new().await?.into(), Self::Sftp(p) => p.create_new().await?.into(), }) } @@ -100,6 +111,7 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.hard_link(to).await, + Self::Lua(p) => p.hard_link(to).await, Self::Sftp(p) => p.hard_link(to).await, } } @@ -107,18 +119,17 @@ impl<'a> Provider for Providers<'a> { async fn metadata(&self) -> io::Result { match self { Self::Local(p) => p.metadata().await, + Self::Lua(p) => p.metadata().await, Self::Sftp(p) => p.metadata().await, } } async fn new<'b>(url: Url<'b>) -> io::Result> { - use yazi_shared::scheme::SchemeKind as K; + use yazi_shared::auth::AuthKind as K; Ok(match url.kind() { - K::Regular | K::Search => Self::Me::Local(yazi_fs::provider::local::Local::new(url).await?), - K::Archive => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } + K::Regular | K::Search => Self::Me::Local(yazi_fs::engine::local::Local::new(url).await?), + K::Mount | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?), K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?), }) } @@ -126,6 +137,7 @@ impl<'a> Provider for Providers<'a> { async fn open(&self) -> io::Result { Ok(match self { Self::Local(p) => p.open().await?.into(), + Self::Lua(p) => p.open().await?.into(), Self::Sftp(p) => p.open().await?.into(), }) } @@ -133,6 +145,7 @@ impl<'a> Provider for Providers<'a> { async fn read_dir(self) -> io::Result { Ok(match self { Self::Local(p) => Self::ReadDir::Local(p.read_dir().await?), + Self::Lua(p) => Self::ReadDir::Lua(p.read_dir().await?), Self::Sftp(p) => Self::ReadDir::Sftp(p.read_dir().await?), }) } @@ -140,6 +153,7 @@ impl<'a> Provider for Providers<'a> { async fn read_link(&self) -> io::Result { match self { Self::Local(p) => p.read_link().await, + Self::Lua(p) => p.read_link().await, Self::Sftp(p) => p.read_link().await, } } @@ -147,6 +161,7 @@ impl<'a> Provider for Providers<'a> { async fn remove_dir(&self) -> io::Result<()> { match self { Self::Local(p) => p.remove_dir().await, + Self::Lua(p) => p.remove_dir().await, Self::Sftp(p) => p.remove_dir().await, } } @@ -154,6 +169,7 @@ impl<'a> Provider for Providers<'a> { async fn remove_dir_all(&self) -> io::Result<()> { match self { Self::Local(p) => p.remove_dir_all().await, + Self::Lua(p) => p.remove_dir_all().await, Self::Sftp(p) => p.remove_dir_all().await, } } @@ -161,6 +177,7 @@ impl<'a> Provider for Providers<'a> { async fn remove_file(&self) -> io::Result<()> { match self { Self::Local(p) => p.remove_file().await, + Self::Lua(p) => p.remove_file().await, Self::Sftp(p) => p.remove_file().await, } } @@ -171,14 +188,16 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.rename(to).await, + Self::Lua(p) => p.rename(to).await, Self::Sftp(p) => p.rename(to).await, } } - async fn set_mode(&self, mode: ChaMode) -> io::Result<()> { + async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { match self { - Self::Local(p) => p.set_mode(mode).await, - Self::Sftp(p) => p.set_mode(mode).await, + Self::Local(p) => p.set_attrs(attrs).await, + Self::Lua(p) => p.set_attrs(attrs).await, + Self::Sftp(p) => p.set_attrs(attrs).await, } } @@ -189,6 +208,7 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.symlink(original, is_dir).await, + Self::Lua(p) => p.symlink(original, is_dir).await, Self::Sftp(p) => p.symlink(original, is_dir).await, } } @@ -199,6 +219,7 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.symlink_dir(original).await, + Self::Lua(p) => p.symlink_dir(original).await, Self::Sftp(p) => p.symlink_dir(original).await, } } @@ -209,6 +230,7 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.symlink_file(original).await, + Self::Lua(p) => p.symlink_file(original).await, Self::Sftp(p) => p.symlink_file(original).await, } } @@ -216,6 +238,7 @@ impl<'a> Provider for Providers<'a> { async fn symlink_metadata(&self) -> io::Result { match self { Self::Local(p) => p.symlink_metadata().await, + Self::Lua(p) => p.symlink_metadata().await, Self::Sftp(p) => p.symlink_metadata().await, } } @@ -223,6 +246,7 @@ impl<'a> Provider for Providers<'a> { async fn trash(&self) -> io::Result<()> { match self { Self::Local(p) => p.trash().await, + Self::Lua(p) => p.trash().await, Self::Sftp(p) => p.trash().await, } } @@ -230,6 +254,7 @@ impl<'a> Provider for Providers<'a> { fn url(&self) -> Url<'_> { match self { Self::Local(p) => p.url(), + Self::Lua(p) => p.url(), Self::Sftp(p) => p.url(), } } @@ -240,6 +265,7 @@ impl<'a> Provider for Providers<'a> { { match self { Self::Local(p) => p.write(contents).await, + Self::Lua(p) => p.write(contents).await, Self::Sftp(p) => p.write(contents).await, } } diff --git a/yazi-vfs/src/engine/lua/demand.rs b/yazi-vfs/src/engine/lua/demand.rs new file mode 100644 index 00000000..fee5dd6a --- /dev/null +++ b/yazi-vfs/src/engine/lua/demand.rs @@ -0,0 +1,61 @@ +use std::io; + +use yazi_fs::engine::{Attrs, Engine, FileBuilder}; +use yazi_runner::provider::ProviderJob; +use yazi_shared::url::AsUrl; + +use crate::engine::lua::{File, Lua}; + +#[derive(Clone, Copy, Default)] +pub struct Demand(yazi_fs::engine::Demand); + +impl FileBuilder for Demand { + type File = File; + + fn append(&mut self, append: bool) -> &mut Self { + self.0.append = append; + self + } + + fn attrs(&mut self, attrs: Attrs) -> &mut Self { + self.0.attrs = attrs; + self + } + + fn create(&mut self, create: bool) -> &mut Self { + self.0.create = create; + self + } + + fn create_new(&mut self, create_new: bool) -> &mut Self { + self.0.create_new = create_new; + self + } + + async fn open(&self, url: U) -> io::Result + where + U: AsUrl, + { + let engine = Lua::new(url.as_url()).await?; + let job = + ProviderJob::Open { url: engine.url.to_owned(), attrs: self.0.attrs, demand: self.0 }; + + let pos = engine.call(job).await?.0?; + Ok(File::new(engine.url, engine.run, pos, self.0)) + } + + fn read(&mut self, read: bool) -> &mut Self { + self.0.read = read; + self + } + + fn truncate(&mut self, truncate: bool) -> &mut Self { + self.0.truncate = truncate; + self + } + + fn write(&mut self, write: bool) -> &mut Self { + self.0.write = write; + self + } +} diff --git a/yazi-vfs/src/engine/lua/file.rs b/yazi-vfs/src/engine/lua/file.rs new file mode 100644 index 00000000..b503ddc7 --- /dev/null +++ b/yazi-vfs/src/engine/lua/file.rs @@ -0,0 +1,197 @@ +use std::{io::{self, SeekFrom}, pin::Pin, task::{Context, Poll, ready}}; + +use mlua::BString; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; +use yazi_fs::{cha::Cha, engine::Demand}; +use yazi_runner::{RUNNER, provider::ProviderJob}; +use yazi_shared::{event::Cmd, url::UrlBuf}; + +type Fut = Pin> + Send + Sync + 'static>>; + +pub struct File { + url: UrlBuf, + pos: u64, + run: &'static Cmd, + demand: Demand, + pending_read: Option>, + pending_seek: Option, + pending_write: Option>, +} + +enum SeekState { + NonBlocking(u64), + Blocking(i64, Fut), +} + +impl File { + pub(super) fn new(url: impl Into, run: &'static Cmd, pos: u64, demand: Demand) -> Self { + Self { + url: url.into(), + pos, + run, + demand, + pending_read: None, + pending_seek: None, + pending_write: None, + } + } + + pub async fn set_len(&self, size: u64) -> io::Result<()> { + let url = self.url.clone(); + Ok(RUNNER.provide(self.run, ProviderJob::SetLen { url, size }).await.ok()?) + } + + pub async fn set_attrs(&self, attrs: yazi_fs::engine::Attrs) -> io::Result<()> { + let url = self.url.clone(); + Ok(RUNNER.provide(self.run, ProviderJob::SetAttrs { url, attrs }).await.ok()?) + } + + pub(crate) async fn metadata(&self) -> io::Result { + let url = self.url.clone(); + Ok(RUNNER.provide(self.run, ProviderJob::Metadata { url }).await.0?) + } + + fn write_impl( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bytes: impl FnOnce() -> Vec, + ) -> Poll> { + let me = self.get_mut(); + if !me.demand.write { + return Poll::Ready(Err(io::ErrorKind::PermissionDenied.into())); + } + + let bytes = bytes(); + if bytes.is_empty() { + return Poll::Ready(Ok(0)); + } + + if me.pending_write.is_none() { + let (run, len) = (me.run, bytes.len()); + let job = ProviderJob::Write { url: me.url.clone(), offset: me.pos, bytes }; + me.pending_write = Some(Box::pin(async move { + RUNNER.provide(run, job).await.ok()?; + Ok(len) + })); + } + + let result = ready!(me.pending_write.as_mut().unwrap().as_mut().poll(cx)); + me.pending_write = None; + + Poll::Ready(result.inspect(|&n| { + me.pos = me.pos.checked_add(n as u64).expect("offset overflow"); + })) + } +} + +impl AsyncRead for File { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let me = self.get_mut(); + if !me.demand.read { + return Poll::Ready(Err(io::ErrorKind::PermissionDenied.into())); + } else if buf.remaining() == 0 { + return Poll::Ready(Ok(())); + } + + if me.pending_read.is_none() { + let run = me.run; + let job = + ProviderJob::Read { url: me.url.clone(), offset: me.pos, len: buf.remaining() }; + me.pending_read = Some(Box::pin(async move { Ok(RUNNER.provide(run, job).await.0?) })); + } + + let result = ready!(me.pending_read.as_mut().unwrap().as_mut().poll(cx)); + me.pending_read = None; + + Poll::Ready(result.map(|bytes| { + let n = bytes.len().min(buf.remaining()); + buf.put_slice(&bytes[..n]); + me.pos = me.pos.checked_add(n as u64).expect("offset overflow"); + })) + } +} + +impl AsyncSeek for File { + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { + let me = self.get_mut(); + if me.pending_seek.is_some() { + return Err(io::Error::other("call poll_complete before start_seek")); + } + + me.pending_seek = Some(match position { + SeekFrom::Start(n) => SeekState::NonBlocking(n), + SeekFrom::Current(n) => me + .pos + .checked_add_signed(n) + .map(SeekState::NonBlocking) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek overflow"))?, + SeekFrom::End(n) => { + let run = me.run; + let job = ProviderJob::Metadata { url: me.url.clone() }; + SeekState::Blocking( + n, + Box::pin(async move { Ok(RUNNER.provide::(run, job).await.0?.len) }), + ) + } + }); + + Ok(()) + } + + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let me = self.get_mut(); + let Some(state) = &mut me.pending_seek else { + return Poll::Ready(Ok(me.pos)); + }; + + let result = match state { + SeekState::NonBlocking(n) => Ok(*n), + SeekState::Blocking(n, fut) => ready!(fut.as_mut().poll(cx)).and_then(|len| { + len + .checked_add_signed(*n) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek overflow")) + }), + }; + if let Ok(n) = result { + me.pos = n; + } + + me.pending_seek = None; + Poll::Ready(result) + } +} + +impl AsyncWrite for File { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + self.write_impl(cx, || buf.to_vec()) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + let len = bufs.iter().map(|b| b.len()).sum(); + self.write_impl(cx, || { + let mut bytes = Vec::with_capacity(len); + for b in bufs { + bytes.extend_from_slice(b); + } + bytes + }) + } + + fn is_write_vectored(&self) -> bool { true } +} diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs new file mode 100644 index 00000000..cb4cdf65 --- /dev/null +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -0,0 +1,205 @@ +use std::io; + +use mlua::FromLua; +use tokio::sync::mpsc; +use yazi_binding::MpscTx; +use yazi_config::vfs::{ServiceLua, Vfs}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files}; +use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}}; +use yazi_shared::{event::Cmd, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; + +use crate::engine::lua::ReadDir; + +#[derive(Clone)] +pub struct Lua<'a> { + pub(super) url: Url<'a>, + + pub(super) run: &'static Cmd, +} + +impl<'a> Engine for Lua<'a> { + type Demand = super::Demand; + type File = super::File; + type Me<'b> = Lua<'b>; + type ReadDir = ReadDir; + type UrlCow = UrlCow<'static>; + + async fn absolute(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call::(ProviderJob::Absolute { url }).await?.0?.into()) + } + + async fn canonicalize(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::Canonicalize { url }).await?.0?) + } + + async fn capabilities(&self) -> io::Result { + Ok(self.call(ProviderJob::Capabilities).await?.0?) + } + + async fn casefold(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::Casefold { url }).await?.0?) + } + + async fn copy

(&self, to: P, attrs: Attrs) -> io::Result + where + P: AsPath, + { + let from = self.url.to_owned(); + let to = to.as_path().to_owned(); + + Ok(self.call(ProviderJob::Copy { from, to, attrs }).await?.0?) + } + + fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> + where + P: AsPath, + A: Into, + { + let (tx, rx) = mpsc::channel(20); + let job = ProviderJob::CopyProgressive { + from: self.url.to_owned(), + to: to.as_path().to_owned(), + attrs: attrs.into(), + tx: MpscTx::map(tx.clone(), Ok), + }; + + let run = self.run; + tokio::spawn(async move { + match RUNNER.provide(run, job).await.ok() { + Ok(()) => tx.send(Ok(0)).await.ok(), + Err(e) => tx.send(Err(e.into())).await.ok(), + }; + }); + + Ok(rx) + } + + async fn create_dir(&self) -> io::Result<()> { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::CreateDir { url }).await?.ok()?) + } + + async fn hard_link

(&self, to: P) -> io::Result<()> + where + P: AsPath, + { + let from = self.url.to_owned(); + let to = to.as_path().to_owned(); + + Ok(self.call(ProviderJob::HardLink { from, to }).await?.ok()?) + } + + async fn metadata(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::Metadata { url }).await?.0?) + } + + async fn new<'b>(url: Url<'b>) -> io::Result> { + let (Url::Mount { auth, .. } | Url::Scope { auth, .. }) = url else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Not a custom VFS URL: {url:?}"), + )); + }; + + let service = Vfs::service::<&ServiceLua>(&auth.domain)?; + if service.scheme != auth.scheme { + return Err(io::Error::other(format!("No such custom VFS authority: {auth}"))); + } + + Ok(Self::Me { url, run: &service.run }) + } + + async fn read_dir(self) -> io::Result { + let url = self.url.to_owned(); + let files: Files = self.call(ProviderJob::ReadDir { url }).await?.0?; + + Ok(ReadDir { files: files.0.into_iter() }) + } + + async fn read_link(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::ReadLink { url }).await?.0?) + } + + async fn remove_dir(&self) -> io::Result<()> { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::RemoveDir { url }).await?.ok()?) + } + + async fn remove_file(&self) -> io::Result<()> { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::RemoveFile { url }).await?.ok()?) + } + + async fn rename

(&self, to: P) -> io::Result<()> + where + P: AsPath, + { + let from = self.url.to_owned(); + let to = to.as_path().to_owned(); + + Ok(self.call(ProviderJob::Rename { from, to }).await?.ok()?) + } + + async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::SetAttrs { url, attrs }).await?.ok()?) + } + + async fn symlink(&self, original: S, is_dir: F) -> io::Result<()> + where + S: AsStrand, + F: AsyncFnOnce() -> io::Result, + { + let original = original.as_strand().encoded_bytes().to_vec(); + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::Symlink { original, url, is_dir: is_dir().await? }).await?.ok()?) + } + + async fn symlink_metadata(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::SymlinkMetadata { url }).await?.0?) + } + + async fn trash(&self) -> io::Result<()> { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::Trash { url }).await?.ok()?) + } + + fn url(&self) -> Url<'_> { self.url.as_url() } + + async fn write(&self, contents: C) -> io::Result<()> + where + C: AsRef<[u8]>, + { + let url = self.url.to_owned(); + let bytes = contents.as_ref().to_vec(); + + Ok(self.call(ProviderJob::Write { url, offset: 0, bytes }).await?.ok()?) + } +} + +impl<'a> Lua<'a> { + pub(super) async fn call(&self, job: ProviderJob) -> io::Result> + where + T: FromLua + Send + 'static, + { + Ok(RUNNER.provide(self.run, job).await) + } +} diff --git a/yazi-vfs/src/engine/lua/mod.rs b/yazi-vfs/src/engine/lua/mod.rs new file mode 100644 index 00000000..292c8cb8 --- /dev/null +++ b/yazi-vfs/src/engine/lua/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(file demand lua read_dir); diff --git a/yazi-vfs/src/engine/lua/read_dir.rs b/yazi-vfs/src/engine/lua/read_dir.rs new file mode 100644 index 00000000..86a99dea --- /dev/null +++ b/yazi-vfs/src/engine/lua/read_dir.rs @@ -0,0 +1,33 @@ +use std::{io, vec}; + +use yazi_fs::{cha::{Cha, ChaType}, engine::{DirReader, FileHolder}}; +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; + +pub struct ReadDir { + pub(super) files: vec::IntoIter, +} + +impl DirReader for ReadDir { + type Entry = DirEntry; + + async fn next(&mut self) -> io::Result> { + Ok(self.files.next().map(|file| DirEntry { file })) + } +} + +// --- Entry +pub struct DirEntry { + file: yazi_fs::file::File, +} + +impl FileHolder for DirEntry { + async fn file_type(&self) -> io::Result { Ok(**self.file.cha) } + + async fn metadata(&self) -> io::Result { Ok(self.file.cha) } + + fn name(&self) -> StrandCow<'_> { self.file.name().unwrap_or_default().into() } + + fn path(&self) -> PathBufDyn { self.file.url.loc().into() } + + fn url(&self) -> UrlBuf { self.file.url.clone() } +} diff --git a/yazi-vfs/src/engine/mod.rs b/yazi-vfs/src/engine/mod.rs new file mode 100644 index 00000000..40539902 --- /dev/null +++ b/yazi-vfs/src/engine/mod.rs @@ -0,0 +1,5 @@ +yazi_macro::mod_pub!(lua sftp); + +yazi_macro::mod_flat!(absolute calculator copier dir_entry demand engine engines read_dir rw_file); + +pub(super) fn init() { sftp::init(); } diff --git a/yazi-vfs/src/provider/read_dir.rs b/yazi-vfs/src/engine/read_dir.rs similarity index 68% rename from yazi-vfs/src/provider/read_dir.rs rename to yazi-vfs/src/engine/read_dir.rs index 55e63159..ba06a59d 100644 --- a/yazi-vfs/src/provider/read_dir.rs +++ b/yazi-vfs/src/engine/read_dir.rs @@ -1,9 +1,10 @@ use std::io; -use yazi_fs::provider::DirReader; +use yazi_fs::engine::DirReader; pub enum ReadDir { - Local(yazi_fs::provider::local::ReadDir), + Local(yazi_fs::engine::local::ReadDir), + Lua(super::lua::ReadDir), Sftp(super::sftp::ReadDir), } @@ -13,6 +14,7 @@ impl DirReader for ReadDir { async fn next(&mut self) -> io::Result> { Ok(match self { Self::Local(reader) => reader.next().await?.map(Self::Entry::Local), + Self::Lua(reader) => reader.next().await?.map(Self::Entry::Lua), Self::Sftp(reader) => reader.next().await?.map(Self::Entry::Sftp), }) } diff --git a/yazi-vfs/src/provider/rw_file.rs b/yazi-vfs/src/engine/rw_file.rs similarity index 67% rename from yazi-vfs/src/provider/rw_file.rs rename to yazi-vfs/src/engine/rw_file.rs index 0b4e6e0d..20809cf5 100644 --- a/yazi-vfs/src/provider/rw_file.rs +++ b/yazi-vfs/src/engine/rw_file.rs @@ -1,11 +1,14 @@ use std::{io, pin::Pin}; -use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; -use yazi_fs::provider::Attrs; +use mlua::{IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt}; +use yazi_binding::Error; +use yazi_fs::engine::Attrs; pub enum RwFile { Tokio(tokio::fs::File), Sftp(Box), + Lua(super::lua::File), } impl From for RwFile { @@ -16,12 +19,17 @@ impl From for RwFile { fn from(f: yazi_sftp::fs::File) -> Self { Self::Sftp(Box::new(f)) } } +impl From for RwFile { + fn from(f: super::lua::File) -> Self { Self::Lua(f) } +} + impl RwFile { // FIXME: path pub async fn metadata(&self) -> io::Result { Ok(match self { Self::Tokio(f) => yazi_fs::cha::Cha::new("// FIXME", f.metadata().await?), Self::Sftp(f) => super::sftp::Cha::try_from(("// FIXME".as_bytes(), &f.fstat().await?))?.0, + Self::Lua(f) => f.metadata().await?, }) } @@ -45,6 +53,7 @@ impl RwFile { f.fsetstat(&attrs).await?; } } + Self::Lua(f) => f.set_attrs(attrs).await?, } Ok(()) @@ -56,6 +65,7 @@ impl RwFile { Self::Sftp(f) => { f.fsetstat(&yazi_sftp::fs::Attrs { size: Some(size), ..Default::default() }).await? } + Self::Lua(f) => f.set_len(size).await?, }) } } @@ -70,6 +80,7 @@ impl AsyncRead for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).poll_read(cx, buf), Self::Sftp(f) => Pin::new(f).poll_read(cx, buf), + Self::Lua(f) => Pin::new(f).poll_read(cx, buf), } } } @@ -80,6 +91,7 @@ impl AsyncSeek for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).start_seek(position), Self::Sftp(f) => Pin::new(f).start_seek(position), + Self::Lua(f) => Pin::new(f).start_seek(position), } } @@ -91,6 +103,7 @@ impl AsyncSeek for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).poll_complete(cx), Self::Sftp(f) => Pin::new(f).poll_complete(cx), + Self::Lua(f) => Pin::new(f).poll_complete(cx), } } } @@ -105,6 +118,7 @@ impl AsyncWrite for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).poll_write(cx, buf), Self::Sftp(f) => Pin::new(f).poll_write(cx, buf), + Self::Lua(f) => Pin::new(f).poll_write(cx, buf), } } @@ -116,6 +130,7 @@ impl AsyncWrite for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).poll_flush(cx), Self::Sftp(f) => Pin::new(f).poll_flush(cx), + Self::Lua(f) => Pin::new(f).poll_flush(cx), } } @@ -127,6 +142,7 @@ impl AsyncWrite for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).poll_shutdown(cx), Self::Sftp(f) => Pin::new(f).poll_shutdown(cx), + Self::Lua(f) => Pin::new(f).poll_shutdown(cx), } } @@ -139,6 +155,7 @@ impl AsyncWrite for RwFile { match &mut *self { Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs), Self::Sftp(f) => Pin::new(f).poll_write_vectored(cx, bufs), + Self::Lua(f) => Pin::new(f).poll_write_vectored(cx, bufs), } } @@ -147,6 +164,34 @@ impl AsyncWrite for RwFile { match self { Self::Tokio(f) => f.is_write_vectored(), Self::Sftp(f) => f.is_write_vectored(), + Self::Lua(f) => f.is_write_vectored(), } } } + +impl UserData for RwFile { + fn add_methods>(methods: &mut M) { + methods.add_async_method_mut("flush", |lua, mut me, ()| async move { + match me.flush().await { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + methods.add_async_method_mut("read", |lua, mut me, len: usize| async move { + let mut buf = vec![0; len]; + match me.read(&mut buf).await { + Ok(n) => { + buf.truncate(n); + lua.create_external_string(buf)?.into_lua_multi(&lua) + } + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + methods.add_async_method_mut("write_all", |lua, mut me, src: LuaString| async move { + match me.write_all(&src.as_bytes()).await { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + } +} diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/engine/sftp/conn.rs similarity index 98% rename from yazi-vfs/src/provider/sftp/conn.rs rename to yazi-vfs/src/engine/sftp/conn.rs index 413f41db..bd0519d9 100644 --- a/yazi-vfs/src/provider/sftp/conn.rs +++ b/yazi-vfs/src/engine/sftp/conn.rs @@ -2,13 +2,11 @@ use std::{io, sync::Arc, time::{Duration, SystemTime}}; use chrono::DateTime; use russh::keys::{PrivateKeyWithHashAlg, agent::AgentIdentity}; -use yazi_fs::provider::local::Local; - -use crate::config::ServiceSftp; +use yazi_config::vfs::ServiceSftp; +use yazi_fs::engine::local::Local; #[derive(Clone, Copy)] pub(super) struct Conn { - pub(super) name: &'static str, pub(super) config: &'static ServiceSftp, } @@ -35,7 +33,7 @@ impl deadpool::managed::Manager for Conn { async fn create(&self) -> Result { let channel = self.connect().await.map_err(|e| { - io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name)) + io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.config.domain)) })?; let mut op = yazi_sftp::Operator::make(channel.into_stream()); diff --git a/yazi-vfs/src/provider/sftp/gate.rs b/yazi-vfs/src/engine/sftp/demand.rs similarity index 55% rename from yazi-vfs/src/provider/sftp/gate.rs rename to yazi-vfs/src/engine/sftp/demand.rs index 5f4a4b7d..3bcf43e9 100644 --- a/yazi-vfs/src/provider/sftp/gate.rs +++ b/yazi-vfs/src/engine/sftp/demand.rs @@ -1,59 +1,59 @@ use std::io; -use yazi_fs::provider::{Attrs, FileBuilder}; +use yazi_fs::engine::{Attrs, Engine, FileBuilder}; use yazi_sftp::fs::Flags; -use yazi_shared::url::{AsUrl, Url}; +use yazi_shared::url::AsUrl; -use crate::{config::{ServiceSftp, Vfs}, provider::sftp::Conn}; +use crate::engine::sftp::Sftp; #[derive(Clone, Copy, Default)] -pub struct Gate(crate::provider::Gate); +pub struct Demand(yazi_fs::engine::Demand); -impl From for Flags { - fn from(Gate(g): Gate) -> Self { +impl From for Flags { + fn from(Demand(demand): Demand) -> Self { let mut flags = Self::empty(); - if g.append { + if demand.append { flags |= Self::APPEND; } - if g.create { + if demand.create { flags |= Self::CREATE; } - if g.create_new { + if demand.create_new { flags |= Self::CREATE | Self::EXCLUDE; } - if g.read { + if demand.read { flags |= Self::READ; } - if g.truncate { + if demand.truncate { flags |= Self::TRUNCATE; } - if g.write { + if demand.write { flags |= Self::WRITE; } flags } } -impl FileBuilder for Gate { +impl FileBuilder for Demand { type File = yazi_sftp::fs::File; fn append(&mut self, append: bool) -> &mut Self { - self.0.append(append); + self.0.append = append; self } fn attrs(&mut self, attrs: Attrs) -> &mut Self { - self.0.attrs(attrs); + self.0.attrs = attrs; self } fn create(&mut self, create: bool) -> &mut Self { - self.0.create(create); + self.0.create = create; self } fn create_new(&mut self, create_new: bool) -> &mut Self { - self.0.create_new(create_new); + self.0.create_new = create_new; self } @@ -61,21 +61,17 @@ impl FileBuilder for Gate { where U: AsUrl, { - let url = url.as_url(); - let (path, (name, config)) = match url { - Url::Sftp { loc, domain } => (*loc, Vfs::service::<&ServiceSftp>(domain).await?), - _ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not an SFTP URL: {url:?}")))?, - }; + let engine = Sftp::new(url.as_url()).await?; + let conn = engine.op().await?; - let conn = Conn { name, config }.roll().await?; let flags = Flags::from(*self); let attrs = super::Attrs(self.0.attrs).try_into().unwrap_or_default(); + let result = conn.open(engine.path, flags, &attrs).await; - let result = conn.open(path, flags, &attrs).await; if self.0.create_new && let Err(yazi_sftp::Error::Status(status)) = &result && status.is_failure() - && conn.lstat(path).await.is_ok() + && conn.lstat(engine.path).await.is_ok() { return Err(io::Error::from(io::ErrorKind::AlreadyExists)); } @@ -84,17 +80,17 @@ impl FileBuilder for Gate { } fn read(&mut self, read: bool) -> &mut Self { - self.0.read(read); + self.0.read = read; self } fn truncate(&mut self, truncate: bool) -> &mut Self { - self.0.truncate(truncate); + self.0.truncate = truncate; self } fn write(&mut self, write: bool) -> &mut Self { - self.0.write(write); + self.0.write = write; self } } diff --git a/yazi-vfs/src/provider/sftp/metadata.rs b/yazi-vfs/src/engine/sftp/metadata.rs similarity index 96% rename from yazi-vfs/src/provider/sftp/metadata.rs rename to yazi-vfs/src/engine/sftp/metadata.rs index 4f6cbfc2..1d82c4dd 100644 --- a/yazi-vfs/src/provider/sftp/metadata.rs +++ b/yazi-vfs/src/engine/sftp/metadata.rs @@ -3,7 +3,7 @@ use std::{io, time::{Duration, UNIX_EPOCH}}; use yazi_fs::cha::ChaKind; // --- Attrs -pub(crate) struct Attrs(pub(crate) yazi_fs::provider::Attrs); +pub(crate) struct Attrs(pub(crate) yazi_fs::engine::Attrs); impl TryFrom for yazi_sftp::fs::Attrs { type Error = (); diff --git a/yazi-vfs/src/engine/sftp/mod.rs b/yazi-vfs/src/engine/sftp/mod.rs new file mode 100644 index 00000000..49e56591 --- /dev/null +++ b/yazi-vfs/src/engine/sftp/mod.rs @@ -0,0 +1,12 @@ +yazi_macro::mod_flat!(conn demand metadata read_dir sftp); + +static CONN: yazi_shim::cell::RoCell< + parking_lot::Mutex< + hashbrown::HashMap< + &'static yazi_config::vfs::ServiceSftp, + &'static deadpool::managed::Pool, + >, + >, +> = yazi_shim::cell::RoCell::new(); + +pub(super) fn init() { CONN.init(Default::default()); } diff --git a/yazi-vfs/src/provider/sftp/read_dir.rs b/yazi-vfs/src/engine/sftp/read_dir.rs similarity index 95% rename from yazi-vfs/src/provider/sftp/read_dir.rs rename to yazi-vfs/src/engine/sftp/read_dir.rs index de55a16d..0d5b0100 100644 --- a/yazi-vfs/src/provider/sftp/read_dir.rs +++ b/yazi-vfs/src/engine/sftp/read_dir.rs @@ -1,6 +1,6 @@ use std::{io, sync::Arc}; -use yazi_fs::provider::{DirReader, FileHolder}; +use yazi_fs::engine::{DirReader, FileHolder}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use super::{Cha, ChaMode}; diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/engine/sftp/sftp.rs similarity index 75% rename from yazi-vfs/src/provider/sftp/sftp.rs rename to yazi-vfs/src/engine/sftp/sftp.rs index a12690b2..40820533 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/engine/sftp/sftp.rs @@ -1,31 +1,31 @@ use std::{io, sync::Arc}; use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; -use yazi_fs::{cha::ChaMode, provider::{Capabilities, DirReader, FileHolder, Provider}}; +use yazi_config::vfs::{ServiceSftp, Vfs}; +use yazi_fs::engine::{Capabilities, DirReader, Engine, FileHolder}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; -use crate::{config::{ServiceSftp, Vfs}, provider::sftp::Conn}; +use crate::engine::sftp::Conn; #[derive(Clone)] pub struct Sftp<'a> { - url: Url<'a>, - path: &'a typed_path::UnixPath, + url: Url<'a>, + pub(super) path: &'a typed_path::UnixPath, - name: &'static str, config: &'static ServiceSftp, } -impl<'a> Provider for Sftp<'a> { +impl<'a> Engine for Sftp<'a> { + type Demand = super::Demand; type File = yazi_sftp::fs::File; - type Gate = super::Gate; type Me<'b> = Sftp<'b>; type ReadDir = super::ReadDir; type UrlCow = UrlCow<'a>; async fn absolute(&self) -> io::Result { - Ok(if let Some(u) = super::try_absolute(self.url) { + Ok(if let Some(u) = crate::engine::try_absolute_impl(self.url) { u } else { self.canonicalize().await?.into() @@ -34,12 +34,19 @@ impl<'a> Provider for Sftp<'a> { async fn canonicalize(&self) -> io::Result { Ok(UrlBuf::Sftp { - loc: self.op().await?.realpath(self.path).await?.into(), - domain: self.name.intern(), + loc: self.op().await?.realpath(self.path).await?.into(), + auth: self.config.auth.clone(), }) } - fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } } + async fn capabilities(&self) -> io::Result { + Ok(Capabilities { + symlink: true, + hard_link: true, + copy_progressive: true, + ..Default::default() + }) + } async fn casefold(&self) -> io::Result { let Some((parent, name)) = self.url.parent().zip(self.url.name()) else { @@ -74,7 +81,7 @@ impl<'a> Provider for Sftp<'a> { .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound)) } - async fn copy

(&self, to: P, attrs: yazi_fs::provider::Attrs) -> io::Result + async fn copy

(&self, to: P, attrs: yazi_fs::engine::Attrs) -> io::Result where P: AsPath, { @@ -98,21 +105,21 @@ impl<'a> Provider for Sftp<'a> { Ok(written) } - fn copy_with_progress(&self, to: P, attrs: A) -> io::Result>> + fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where P: AsPath, - A: Into, + A: Into, { let to = UrlBuf::Sftp { - loc: LocBuf::::saturated( + loc: LocBuf::::saturated( to.as_path().to_unix_owned()?, - SchemeKind::Sftp, + AuthKind::Sftp, ), - domain: self.name.intern(), + auth: self.config.auth.clone(), }; let from = self.url.to_owned(); - Ok(crate::provider::copy_with_progress_impl(from, to, attrs.into())) + Ok(crate::engine::copy_progressive_impl(from, to, attrs.into())) } async fn create_dir(&self) -> io::Result<()> { @@ -144,15 +151,12 @@ impl<'a> Provider for Sftp<'a> { } async fn new<'b>(url: Url<'b>) -> io::Result> { - match url { - Url::Regular(_) | Url::Search { .. } | Url::Archive { .. } => { - Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))) - } - Url::Sftp { loc, domain } => { - let (name, config) = Vfs::service::<&ServiceSftp>(domain).await?; - Ok(Self::Me { url, path: loc.as_inner(), name, config }) - } - } + let Url::Sftp { loc, auth } = url else { + return Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))); + }; + + let config = Vfs::service::<&ServiceSftp>(&auth.domain)?; + Ok(Self::Me { url, path: loc.as_inner(), config }) } async fn read_dir(self) -> io::Result { @@ -192,10 +196,10 @@ impl<'a> Provider for Sftp<'a> { Ok(()) } - async fn set_mode(&self, mode: ChaMode) -> io::Result<()> { - let attrs = super::Attrs(yazi_fs::provider::Attrs { mode: Some(mode), ..Default::default() }) + async fn set_attrs(&self, attrs: yazi_fs::engine::Attrs) -> io::Result<()> { + let attrs = super::Attrs(attrs) .try_into() - .map_err(|()| io::Error::new(io::ErrorKind::InvalidInput, "Cannot convert mode"))?; + .map_err(|()| io::Error::new(io::ErrorKind::InvalidInput, "Cannot convert attributes"))?; Ok(self.op().await?.setstat(self.path, attrs).await?) } @@ -226,6 +230,6 @@ impl<'a> Provider for Sftp<'a> { impl<'a> Sftp<'a> { #[inline] pub(super) async fn op(&self) -> io::Result> { - Conn { name: self.name, config: self.config }.roll().await + Conn { config: self.config }.roll().await } } diff --git a/yazi-vfs/src/entries.rs b/yazi-vfs/src/entries.rs index da47837b..290841a6 100644 --- a/yazi-vfs/src/entries.rs +++ b/yazi-vfs/src/entries.rs @@ -1,10 +1,10 @@ use std::io; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::{Entries, FilesOp, cha::Cha, file::File, mounts::PARTITIONS, provider::{DirReader, FileHolder}}; +use yazi_fs::{Entries, FilesOp, cha::Cha, engine::{DirReader, FileHolder}, file::File, mounts::PARTITIONS}; use yazi_shared::url::UrlBuf; -use crate::{VfsCha, VfsFile, VfsFilesOp, provider::{self, DirEntry}}; +use crate::{VfsCha, VfsFile, VfsFilesOp, engine::{self, DirEntry}}; pub trait VfsEntries { fn from_dir(dir: &UrlBuf) -> impl Future>>; @@ -16,7 +16,7 @@ pub trait VfsEntries { impl VfsEntries for Entries { async fn from_dir(dir: &UrlBuf) -> std::io::Result> { - let mut it = provider::read_dir(dir).await?; + let mut it = engine::read_dir(dir).await?; let (tx, rx) = mpsc::unbounded_channel(); tokio::spawn(async move { @@ -37,7 +37,7 @@ impl VfsEntries for Entries { } async fn from_dir_bulk(dir: &UrlBuf) -> std::io::Result> { - let mut it = provider::read_dir(dir).await?; + let mut it = engine::read_dir(dir).await?; let mut entries = Vec::new(); while let Ok(Some(entry)) = it.next().await { entries.push(entry); diff --git a/yazi-vfs/src/file.rs b/yazi-vfs/src/file.rs index 26323a2d..8b4a7d72 100644 --- a/yazi-vfs/src/file.rs +++ b/yazi-vfs/src/file.rs @@ -3,7 +3,7 @@ use std::io; use yazi_fs::{cha::Cha, file::File}; use yazi_shared::url::{UrlBuf, UrlCow}; -use crate::{VfsCha, provider}; +use crate::{VfsCha, engine}; pub trait VfsFile: Sized { fn new<'a>(url: impl Into>) -> impl Future>; @@ -16,13 +16,13 @@ pub trait VfsFile: Sized { impl VfsFile for File { async fn new<'a>(url: impl Into>) -> io::Result { let url = url.into(); - let cha = provider::symlink_metadata(&url).await?; + let cha = engine::symlink_metadata(&url).await?; Ok(Self::from_follow(url.into_owned(), cha).await) } async fn maybe_new<'a>(url: impl Into>) -> io::Result> { let url = url.into(); - let cha = match provider::symlink_metadata(&url).await { + let cha = match engine::symlink_metadata(&url).await { Ok(cha) => cha, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), Err(e) => return Err(e), @@ -31,7 +31,7 @@ impl VfsFile for File { } async fn from_follow(url: UrlBuf, cha: Cha) -> Self { - let link_to = if cha.is_link() { provider::read_link(&url).await.ok() } else { None }; + let link_to = if cha.is_link() { engine::read_link(&url).await.ok() } else { None }; let cha = Cha::from_follow(&url, cha).await; diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index 445b635f..733fc5b5 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -2,21 +2,18 @@ use std::io::{self}; use yazi_shared::{strand::{StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; -use crate::provider; +use crate::engine; pub async fn maybe_exists(url: impl AsUrl) -> bool { - match provider::symlink_metadata(url).await { + match engine::symlink_metadata(url).await { Ok(_) => true, Err(e) => e.kind() != io::ErrorKind::NotFound, } } pub async fn unique_file(u: UrlBuf, is_dir: bool) -> io::Result { - let result = if is_dir { - provider::create_dir(&u).await - } else { - provider::create_new(&u).await.map(|_| ()) - }; + let result = + if is_dir { engine::create_dir(&u).await } else { engine::create_new(&u).await.map(|_| ()) }; match result { Ok(()) => Ok(u), @@ -55,9 +52,9 @@ async fn _unique_file(mut url: UrlBuf, is_dir: bool) -> io::Result { url.try_set_name(&name)?; let result = if is_dir { - provider::create_dir(&url).await + engine::create_dir(&url).await } else { - provider::create_new(&url).await.map(|_| ()) + engine::create_new(&url).await.map(|_| ()) }; match result { diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index 79d5c69a..fb81f98b 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,5 +1,5 @@ -yazi_macro::mod_pub!(config provider); +yazi_macro::mod_pub!(engine); yazi_macro::mod_flat!(cha entries file fns op); -pub fn init() { provider::init(); } +pub fn init() { engine::init(); } diff --git a/yazi-vfs/src/provider/lua.rs b/yazi-vfs/src/provider/lua.rs deleted file mode 100644 index e61ba9bd..00000000 --- a/yazi-vfs/src/provider/lua.rs +++ /dev/null @@ -1,32 +0,0 @@ -use mlua::{IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use yazi_binding::Error; - -use crate::provider::RwFile; - -impl UserData for RwFile { - fn add_methods>(methods: &mut M) { - methods.add_async_method_mut("flush", |lua, mut me, ()| async move { - match me.flush().await { - Ok(()) => true.into_lua_multi(&lua), - Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), - } - }); - methods.add_async_method_mut("read", |lua, mut me, len: usize| async move { - let mut buf = vec![0; len]; - match me.read(&mut buf).await { - Ok(n) => { - buf.truncate(n); - lua.create_external_string(buf)?.into_lua_multi(&lua) - } - Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), - } - }); - methods.add_async_method_mut("write_all", |lua, mut me, src: LuaString| async move { - match me.write_all(&src.as_bytes()).await { - Ok(()) => true.into_lua_multi(&lua), - Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), - } - }); - } -} diff --git a/yazi-vfs/src/provider/mod.rs b/yazi-vfs/src/provider/mod.rs deleted file mode 100644 index b8afef8f..00000000 --- a/yazi-vfs/src/provider/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -yazi_macro::mod_pub!(sftp); - -yazi_macro::mod_flat!(calculator copier dir_entry gate lua provider providers read_dir rw_file); - -pub(super) fn init() { sftp::init(); } diff --git a/yazi-vfs/src/provider/sftp/mod.rs b/yazi-vfs/src/provider/sftp/mod.rs deleted file mode 100644 index 8bf7b524..00000000 --- a/yazi-vfs/src/provider/sftp/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -yazi_macro::mod_flat!(absolute conn gate metadata read_dir sftp); - -static CONN: yazi_shim::cell::RoCell< - parking_lot::Mutex< - hashbrown::HashMap<&'static crate::config::ServiceSftp, &'static deadpool::managed::Pool>, - >, -> = yazi_shim::cell::RoCell::new(); - -pub(super) fn init() { CONN.init(Default::default()); } diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 5392c22f..6c817473 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -5,7 +5,7 @@ use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Result, Watcher}; use tokio::{pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tracing::error; -use yazi_fs::{FilesOp, file::File, mounts::PARTITIONS, provider::{self, Provider}}; +use yazi_fs::{FilesOp, engine::{self, Engine}, file::File, mounts::PARTITIONS}; use yazi_shared::url::{UrlBuf, UrlLike}; use yazi_vfs::VfsFile; @@ -79,7 +79,7 @@ impl Local { return true; } - match provider::local::Local::regular(path).metadata().await { + match engine::local::Local::regular(path).metadata().await { Ok(cha) => PARTITIONS.read().soundless(cha), Err(_) => true, } @@ -104,7 +104,7 @@ impl Local { }; if let Some(p) = file.url.as_local() - && !provider::local::match_name_case(p).await + && !engine::local::match_name_case(p).await { ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); continue; diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 7d74b1a3..1c4c5336 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use percent_encoding::percent_decode; use tokio::sync::mpsc; -use yazi_shared::{scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{auth::AuthKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{WATCHED, local::LINKED}; @@ -20,9 +20,9 @@ impl Reporter { { for url in urls.into_iter().map(Into::into) { match url.as_url().kind() { - SchemeKind::Regular | SchemeKind::Search => self.report_local(url), - SchemeKind::Archive => {} - SchemeKind::Sftp => self.report_remote(url), + AuthKind::Regular | AuthKind::Search => self.report_local(url), + AuthKind::Mount => {} // TODO: mounted VFS cache invalidation + AuthKind::Scope | AuthKind::Sftp => self.report_remote(url), } } } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index f74f7a47..6628191b 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -1,9 +1,8 @@ use std::{ops::{Deref, DerefMut}, path::Path}; use hashbrown::HashSet; -use percent_encoding::percent_decode_str; use yazi_fs::{Xdg, path::PercentEncoding}; -use yazi_shared::{path::{Component, PathBufDyn, PathDyn, PathLike}, pool::InternStr, scheme::SchemeKind, url::{AsUrl, UrlBuf}}; +use yazi_shared::{auth::{Auth, AuthKind}, path::{Component, PathBufDyn, PathDyn, PathLike}, url::{AsUrl, UrlBuf}}; use crate::Watchee; @@ -42,21 +41,26 @@ impl Watched { pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option { let mut it = cache.try_strip_prefix(Xdg::temp_dir()).ok()?.components(); - // Parse domain - let domain = it.next()?.as_normal()?.to_str().ok()?; - let domain = percent_decode_str(domain.strip_prefix("sftp-")?).decode_utf8().ok()?.intern(); + // Parse authority + let cache = it.next()?.as_normal()?.to_str().ok()?; + let auth = Auth::parse_cache(cache).ok()?; // Parse path let (path, abs) = if let Ok(p) = it.path().try_strip_prefix(".%2F") { (p, false) } else { (it.path(), true) }; - let path = path.percent_decode(SchemeKind::Sftp).ok()?; + let path = path.percent_decode(auth.kind).ok()?; let path = PathBufDyn::from_components( - SchemeKind::Sftp, + auth.kind, abs.then_some(Component::RootDir).into_iter().chain(path.components()), ) .ok()?; - let url = UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), domain }; + let url = match auth.kind { + AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth }, + AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth }, + AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth }, + AuthKind::Regular | AuthKind::Search => return None, + }; if self.contains_url(&url) { Some(url) } else { None } } } diff --git a/yazi-widgets/Cargo.toml b/yazi-widgets/Cargo.toml index 0e7dee39..538983f1 100644 --- a/yazi-widgets/Cargo.toml +++ b/yazi-widgets/Cargo.toml @@ -18,7 +18,6 @@ vendored-lua = [ "mlua/vendored" ] [dependencies] yazi-binding = { path = "../yazi-binding", version = "26.5.6" } -yazi-dds = { path = "../yazi-dds", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } diff --git a/yazi-widgets/src/input/input_arc.rs b/yazi-widgets/src/input/input_arc.rs index 71f132ef..8ac68321 100644 --- a/yazi-widgets/src/input/input_arc.rs +++ b/yazi-widgets/src/input/input_arc.rs @@ -4,11 +4,8 @@ use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMetho use parking_lot::Mutex; use ratatui_core::widgets::Widget; use yazi_binding::{elements::{Area, Spatial}, impl_area_method}; -use yazi_dds::Pubsub; -use yazi_macro::err; -use yazi_shim::strum::IntoStr; -use crate::input::{Input, InputOpt, InputStyles}; +use crate::input::{Input, InputEvent, InputOpt, InputStyles}; #[derive(Clone, Debug, Default)] pub struct InputArc { @@ -25,13 +22,12 @@ impl Deref for InputArc { } impl InputArc { - pub fn compose(lua: &Lua, styles: InputStyles) -> mlua::Result { + pub fn compose(lua: &Lua, styles: InputStyles, cb: fn(InputEvent)) -> mlua::Result { let new = lua.create_function(move |_, (_, mut opt): (Table, InputOpt)| { opt.styles.normal = opt.styles.normal.or(styles.normal); opt.styles.selected = opt.styles.selected.or(styles.selected); opt.styles.blink = opt.styles.blink.or(styles.blink); - opt = opt.with_cb(|e| err!(Pubsub::pub_after_input((&e).into_str(), e.value()))); - Ok(Self::from(Input::new(opt)?)) + Ok(Self::from(Input::new(opt.with_cb(cb))?)) })?; let input = lua.create_table()?; From ddc6632505064ab8b393e247a19b62a4668ed9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 13 Jul 2026 09:01:59 +0800 Subject: [PATCH 04/12] fix: flush last input before submitting the completion (#4119) --- Cargo.lock | 96 ++++++++++++++++----------------- Cargo.toml | 4 +- yazi-actor/src/cmp/close.rs | 18 +++++++ yazi-core/src/which/which.rs | 4 ++ yazi-parser/src/cmp/trigger.rs | 4 ++ yazi-term/src/event/keyboard.rs | 3 +- 6 files changed, 78 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ba1d704..cd76337b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,18 +487,18 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", @@ -519,9 +519,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "castaway" @@ -543,9 +543,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "jobserver", @@ -975,9 +975,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "pem-rfc7468", @@ -1194,9 +1194,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.74.1" +version = "1.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be87932f10230a4339ab394edd8e4611fcb72553d8295b4d52ea55249b3daa5" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" dependencies = [ "bit_field", "half", @@ -1404,9 +1404,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" dependencies = [ "generic-array 0.14.9", "rustversion", @@ -1697,9 +1697,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd854a95a4ac672fed8c054136039fd32c22cf039ff09ead7280afe920486483" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" dependencies = [ "bitflags", "inotify-sys", @@ -1947,9 +1947,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ "hashbrown 0.17.1", ] @@ -1993,9 +1993,9 @@ dependencies = [ [[package]] name = "md5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "memchr" @@ -2370,7 +2370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", - "rand 0.8.6", + "rand 0.8.7", "serde", ] @@ -2624,9 +2624,9 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ "cpufeatures", "universal-hash", @@ -2635,9 +2635,9 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +checksum = "b20f20e954175de5f463f67781b35583397d916b1d148738923711b2ad16bee8" dependencies = [ "cpubits", "cpufeatures", @@ -2828,9 +2828,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", "serde", @@ -2838,9 +2838,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -2960,7 +2960,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha", "simd_helpers", "thiserror 2.0.18", @@ -3059,9 +3059,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -3071,9 +3071,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -3160,7 +3160,7 @@ dependencies = [ "elliptic-curve", "enum_dispatch", "futures", - "generic-array 1.4.3", + "generic-array 1.4.4", "getrandom 0.4.3", "ghash", "hex-literal", @@ -3232,9 +3232,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" @@ -3814,9 +3814,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] @@ -3889,9 +3889,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4091,7 +4091,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" dependencies = [ - "rand 0.9.4", + "rand 0.9.5", ] [[package]] @@ -5445,18 +5445,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -5471,9 +5471,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index a7a57091..a4f5aa33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ image = { version = "0.25.10", default-features = false, features indexmap = { version = "2.14.0", features = [ "serde" ] } inventory = "0.3.24" libc = "0.2.186" -lru = "0.18.0" +lru = "0.18.1" mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] } objc2 = "0.6.4" ordered-float = { version = "5.3.0", features = [ "serde" ] } @@ -65,7 +65,7 @@ percent-encoding = "2.3.2" rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] } ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] } ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] } -regex = "1.12.4" +regex = "1.13.0" russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } diff --git a/yazi-actor/src/cmp/close.rs b/yazi-actor/src/cmp/close.rs index 11851ac7..ebb95926 100644 --- a/yazi-actor/src/cmp/close.rs +++ b/yazi-actor/src/cmp/close.rs @@ -16,6 +16,10 @@ impl Actor for Close { const NAME: &str = "close"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { + if form.submit && cx.cmp.visible { + Self::flush_last_input(cx)?; + } + let cmp = &mut cx.cmp; if let Some(item) = cmp.selected().filter(|_| form.submit).cloned() { return act!(input:complete, cx, CompleteOpt { name: item.name, is_dir: item.is_dir, ticket: cmp.ticket }); @@ -27,3 +31,17 @@ impl Actor for Close { succ!(render!(mem::replace(&mut cmp.visible, false))); } } + +impl Close { + fn flush_last_input(cx: &mut Ctx) -> Result { + let Some(guard) = cx.input.lock() else { succ!() }; + if cx.cmp.ticket == guard.ticket.current() { + succ!(); + } + + let before = guard.partition().0.to_owned(); + drop(guard); + + act!(cmp:trigger, cx, before) + } +} diff --git a/yazi-core/src/which/which.rs b/yazi-core/src/which/which.rs index d310e2e1..de959b51 100644 --- a/yazi-core/src/which/which.rs +++ b/yazi-core/src/which/which.rs @@ -17,6 +17,10 @@ pub struct Which { impl Which { pub fn r#type(&mut self, key: Key) -> bool { + if key.code.is_modifier() { + return false; + } + self.cands.retain(|c| c.on.len() > self.times && c.on[self.times] == key); self.times += 1; diff --git a/yazi-parser/src/cmp/trigger.rs b/yazi-parser/src/cmp/trigger.rs index a1937163..f7171467 100644 --- a/yazi-parser/src/cmp/trigger.rs +++ b/yazi-parser/src/cmp/trigger.rs @@ -8,6 +8,10 @@ pub struct TriggerForm { pub ticket: Option, } +impl From for TriggerForm { + fn from(word: String) -> Self { Self { word: word.into(), ticket: None } } +} + impl From for TriggerForm { fn from(mut a: ActionCow) -> Self { Self { word: a.take_first().unwrap_or_default(), ticket: a.get("ticket").ok() } diff --git a/yazi-term/src/event/keyboard.rs b/yazi-term/src/event/keyboard.rs index 6952eeb8..a5494be8 100644 --- a/yazi-term/src/event/keyboard.rs +++ b/yazi-term/src/event/keyboard.rs @@ -1,6 +1,7 @@ use bitflags::bitflags; use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use strum::EnumIs; use crate::{ParseError, Result, bail, event::Modifiers}; @@ -81,7 +82,7 @@ impl KeyEventState { } // --- Code -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, EnumIs, Eq, Hash, PartialEq, Serialize)] #[serde(tag = "type", content = "value")] pub enum KeyCode { Char(char), From d39637f40c5261620f7b571c9a2501ca361cc0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 13 Jul 2026 20:01:47 +0800 Subject: [PATCH 05/12] feat!: support VFS services with same name but different schemes (#4120) --- CHANGELOG.md | 5 ++- yazi-config/preset/vfs-default.toml | 1 - yazi-config/src/vfs/authorities.rs | 48 ++++++++++++++++++++++ yazi-config/src/vfs/domains.rs | 62 +++++++++++++++++++++++++++++ yazi-config/src/vfs/lua.rs | 2 +- yazi-config/src/vfs/mod.rs | 2 +- yazi-config/src/vfs/services.rs | 47 ---------------------- yazi-config/src/vfs/sftp.rs | 6 +-- yazi-config/src/vfs/vfs.rs | 37 ++++++++--------- yazi-shared/src/auth/auth.rs | 2 - yazi-vfs/src/engine/lua/lua.rs | 6 +-- yazi-vfs/src/engine/sftp/sftp.rs | 2 +- 12 files changed, 138 insertions(+), 82 deletions(-) create mode 100644 yazi-config/src/vfs/authorities.rs create mode 100644 yazi-config/src/vfs/domains.rs delete mode 100644 yazi-config/src/vfs/services.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index deded357..09209fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Changed +- Rename SFTP sections in `vfs.toml` from `[services.domain]` to `[sftp.domain]` ([#4120]). - Rename `` to `` ([#3989]) -- Rename `type` field in `vfs.toml` to `kind`, leaving `type` available for future custom VFS parameters ([#4118]) -- Remove `Url.is_archive` so `archive://` can be registered as a custom scheme ([#4118]) +- Remove `Url.is_archive` - `archive://` is no longer built in and can now be registered by plugins ([#4118]) - Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096]) - Remove `help:filter` action since the filter input is now always available ([#4074]) - `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074]) @@ -1780,3 +1780,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4104]: https://github.com/sxyazi/yazi/pull/4104 [#4108]: https://github.com/sxyazi/yazi/pull/4108 [#4118]: https://github.com/sxyazi/yazi/pull/4118 +[#4120]: https://github.com/sxyazi/yazi/pull/4120 diff --git a/yazi-config/preset/vfs-default.toml b/yazi-config/preset/vfs-default.toml index 48be1dab..e69de29b 100644 --- a/yazi-config/preset/vfs-default.toml +++ b/yazi-config/preset/vfs-default.toml @@ -1 +0,0 @@ -[services] diff --git a/yazi-config/src/vfs/authorities.rs b/yazi-config/src/vfs/authorities.rs new file mode 100644 index 00000000..23ba0a19 --- /dev/null +++ b/yazi-config/src/vfs/authorities.rs @@ -0,0 +1,48 @@ +use hashbrown::HashMap; +use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}}; +use yazi_shared::auth::Scheme; +use yazi_shim::toml::DeserializeOverWith; + +use super::{DomainSeed, Domains, Service}; + +pub struct Authorities(HashMap); + +impl Authorities { + pub fn get(&self, scheme: &Scheme, domain: &str) -> Option<&Service> { + self.0.get(scheme)?.get(domain) + } +} + +impl<'de> Deserialize<'de> for Authorities { + fn deserialize>(deserializer: D) -> Result { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = Authorities; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a map of VFS schemes") + } + + fn visit_map>(self, mut map: M) -> Result { + let mut authorities = HashMap::new(); + while let Some(scheme) = map.next_key()? { + let domains = map.next_value_seed(DomainSeed(&scheme))?; + authorities.insert(scheme, domains); + } + Ok(Authorities(authorities)) + } + } + + deserializer.deserialize_map(V) + } +} + +impl DeserializeOverWith for Authorities { + fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result { + for (scheme, domains) in Self::deserialize(de)?.0 { + self.0.entry(scheme).or_default().extend(domains.0); + } + Ok(self) + } +} diff --git a/yazi-config/src/vfs/domains.rs b/yazi-config/src/vfs/domains.rs new file mode 100644 index 00000000..665e07d3 --- /dev/null +++ b/yazi-config/src/vfs/domains.rs @@ -0,0 +1,62 @@ +use std::{ops::{Deref, DerefMut}, sync::Arc}; + +use hashbrown::HashMap; +use serde::{Deserialize, Deserializer, de::{DeserializeSeed, Error}}; +use yazi_shared::{KebabCasedKey, auth::Scheme}; + +use super::{Service, ServiceSftp}; + +#[derive(Default)] +pub struct Domains(pub(super) HashMap); + +impl Deref for Domains { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Domains { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Domains { + fn init(&mut self, scheme: &Scheme) { + for (domain, service) in &mut self.0 { + let kind = service.kind(); + let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); + + auth.kind = kind; + auth.scheme = scheme.clone(); + auth.domain = domain.clone().into(); + } + } +} + +// --- DomainSeed +pub(super) struct DomainSeed<'a>(pub &'a Scheme); + +impl<'de> DeserializeSeed<'de> for DomainSeed<'_> { + type Value = Domains; + + fn deserialize>(self, deserializer: D) -> Result { + let mut domains = Domains(match self.0 { + Scheme::Regular | Scheme::Search => { + return Err(D::Error::custom("scheme cannot be configured")); + } + Scheme::Sftp => { + let map = HashMap::::deserialize(deserializer)?; + map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect() + } + Scheme::Custom(_) => { + let map = HashMap::::deserialize(deserializer)?; + if map.values().any(|service| matches!(service, Service::Sftp(_))) { + return Err(D::Error::custom("SFTP services must use the `sftp` scheme")); + } + map + } + }); + + domains.init(self.0); + Ok(domains) + } +} diff --git a/yazi-config/src/vfs/lua.rs b/yazi-config/src/vfs/lua.rs index f94e2d05..f46f5721 100644 --- a/yazi-config/src/vfs/lua.rs +++ b/yazi-config/src/vfs/lua.rs @@ -5,7 +5,7 @@ use yazi_shared::{auth::Auth, event::Cmd}; #[derive(Deserialize)] pub struct ServiceLua { - #[serde(flatten)] + #[serde(skip, default)] pub auth: Arc, pub run: Cmd, } diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs index 4481f651..4bb8af1a 100644 --- a/yazi-config/src/vfs/mod.rs +++ b/yazi-config/src/vfs/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(lua service services sftp vfs); +yazi_macro::mod_flat!(authorities domains lua service sftp vfs); diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs deleted file mode 100644 index 46c1d1c8..00000000 --- a/yazi-config/src/vfs/services.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::{ops::{Deref, DerefMut}, sync::Arc}; - -use hashbrown::HashMap; -use serde::{Deserialize, Deserializer}; -use yazi_shared::KebabCasedKey; -use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith}; - -use super::Service; - -pub struct Services(HashMap); - -impl Deref for Services { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Services { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - -impl<'de> Deserialize<'de> for Services { - fn deserialize>(deserializer: D) -> Result { - Self(HashMap::deserialize(deserializer)?) - .deserialize_over_hook() - .map_err(serde::de::Error::custom) - } -} - -impl DeserializeOverWith for Services { - fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result { - Ok(Self(self.0.deserialize_over_with(de)?)) - } -} - -impl DeserializeOverHook for Services { - fn deserialize_over_hook(mut self) -> Result { - for (domain, service) in &mut self.0 { - let kind = service.kind(); - let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); - - auth.kind = kind; - auth.domain = domain.to_string().into(); - } - Ok(self) - } -} diff --git a/yazi-config/src/vfs/sftp.rs b/yazi-config/src/vfs/sftp.rs index 6f3a5615..945c75a1 100644 --- a/yazi-config/src/vfs/sftp.rs +++ b/yazi-config/src/vfs/sftp.rs @@ -2,11 +2,11 @@ use std::{env, ops::Deref, path::PathBuf, sync::Arc}; use serde::{Deserialize, Deserializer, Serialize, de}; use yazi_fs::path::sanitize_path; -use yazi_shared::auth::{Auth, AuthKind, Scheme}; +use yazi_shared::auth::Auth; #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ServiceSftp { - #[serde(skip, default = "default_auth")] + #[serde(skip, default)] pub auth: Arc, pub host: String, pub user: String, @@ -29,8 +29,6 @@ impl Deref for ServiceSftp { fn deref(&self) -> &Self::Target { &self.auth } } -fn default_auth() -> Arc { Auth::new(AuthKind::Sftp, Scheme::Sftp, "") } - fn deserialize_path<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 7d936147..6ed4ca24 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -1,31 +1,33 @@ use std::io; use anyhow::{Context, Result}; -use serde::Deserialize; -use yazi_codegen::{DeserializeOver, DeserializeOver1}; +use serde::{Deserialize, Deserializer}; +use yazi_codegen::DeserializeOver; use yazi_fs::{Xdg, ok_or_not_found}; -use yazi_shared::auth::AuthInventory; +use yazi_shared::auth::{Auth, AuthInventory}; +use yazi_shim::toml::DeserializeOverWith; -use super::{Service, Services}; +use super::{Authorities, Service}; use crate::VFS; -#[derive(Deserialize, DeserializeOver, DeserializeOver1)] +#[derive(Deserialize, DeserializeOver)] pub struct Vfs { - pub services: Services, + #[serde(flatten)] + pub authorities: Authorities, } impl Vfs { - pub fn service

(domain: &str) -> io::Result

+ pub fn service

(auth: &Auth) -> io::Result

where P: TryFrom<&'static Service, Error = &'static str>, { - let Some((key, value)) = VFS.services.get_key_value(domain) else { - return Err(io::Error::other(format!("No such VFS service: {domain}"))); + let Some(value) = VFS.authorities.get(&auth.scheme, &auth.domain) else { + return Err(io::Error::other(format!("No such VFS service: {auth}"))); }; match value.try_into() { Ok(p) => Ok(p), - Err(e) => Err(io::Error::other(format!("VFS service `{key}` has wrong kind: {e}"))), + Err(e) => Err(io::Error::other(format!("VFS service `{auth}` has wrong kind: {e}"))), } } @@ -36,18 +38,17 @@ impl Vfs { } } +impl DeserializeOverWith for Vfs { + fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result { + Ok(Self { authorities: self.authorities.deserialize_over_with(de)? }) + } +} + // --- Inject inventory::submit! { AuthInventory { get: |scheme, domain| { - VFS.services.get(domain).and_then(|service| { - let auth = service.auth(); - if auth.scheme == scheme { - Some(auth.clone()) - } else { - None - } - }) + VFS.authorities.get(scheme, domain).map(|service| service.auth().clone()) }, } } diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index 3a6efe2f..d1027b76 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -11,10 +11,8 @@ pub(super) static DEFAULT_ARC: RoCell> = RoCell::new(); #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] pub struct Auth { - #[serde(default)] pub kind: AuthKind, pub scheme: Scheme, - #[serde(default)] pub domain: SStr, } diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs index cb4cdf65..e9810f63 100644 --- a/yazi-vfs/src/engine/lua/lua.rs +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -110,11 +110,7 @@ impl<'a> Engine for Lua<'a> { )); }; - let service = Vfs::service::<&ServiceLua>(&auth.domain)?; - if service.scheme != auth.scheme { - return Err(io::Error::other(format!("No such custom VFS authority: {auth}"))); - } - + let service = Vfs::service::<&ServiceLua>(auth)?; Ok(Self::Me { url, run: &service.run }) } diff --git a/yazi-vfs/src/engine/sftp/sftp.rs b/yazi-vfs/src/engine/sftp/sftp.rs index 40820533..fca991df 100644 --- a/yazi-vfs/src/engine/sftp/sftp.rs +++ b/yazi-vfs/src/engine/sftp/sftp.rs @@ -155,7 +155,7 @@ impl<'a> Engine for Sftp<'a> { return Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))); }; - let config = Vfs::service::<&ServiceSftp>(&auth.domain)?; + let config = Vfs::service::<&ServiceSftp>(auth)?; Ok(Self::Me { url, path: loc.as_inner(), config }) } From 4dab4803479f5e1343c41daebd7e237b90e3d009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 13 Jul 2026 22:51:42 +0800 Subject: [PATCH 06/12] fix: preserve search results on refresh (#4123) --- yazi-actor/src/mgr/refresh.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index c6692aaa..abbf1888 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -52,8 +52,11 @@ impl Refresh { } } - let futs: Vec<_> = - folders.iter().filter(|&f| f.url.is_absolute()).map(|&f| go(f.url.clone(), f.cha)).collect(); + let futs: Vec<_> = folders + .iter() + .filter(|&f| f.url.is_absolute() && !f.url.is_search()) + .map(|&f| go(f.url.clone(), f.cha)) + .collect(); if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); From b25ae9f82d415da7fbd4d4e4b09397a194540a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 14 Jul 2026 01:11:34 +0800 Subject: [PATCH 07/12] fix: fail when package source is missing (#4124) --- yazi-cli/src/package/delete.rs | 23 ++++++++++------------- yazi-cli/src/package/dependency.rs | 10 ++++++---- yazi-cli/src/package/deploy.rs | 12 ++++++++---- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index 113484ca..0b705494 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -17,8 +17,14 @@ impl Dependency { } self.delete_assets().await?; - self.delete_sources().await?; - Ok(()) + if !self.delete_sources().await? { + outln!( + "For safety, user data will be preserved, manually delete them from: {}", + dir.display() + )?; + } + + Ok(outln!("Done!")?) } pub(super) async fn delete_assets(&self) -> Result<()> { @@ -39,7 +45,7 @@ impl Dependency { Ok(()) } - pub(super) async fn delete_sources(&self) -> Result<()> { + pub(super) async fn delete_sources(&self) -> Result { let dir = self.target(); let files = if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&dir).await? }; @@ -49,15 +55,6 @@ impl Dependency { .with_context(|| format!("failed to delete `{}`", path.display()))?; } - if ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok() { - outln!("Done!")?; - } else { - outln!( - "Done! -For safety, user data has been preserved, please manually delete them within: {}", - dir.display() - )?; - } - Ok(()) + Ok(ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok()) } } diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index 6de5755d..4c76f2cf 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -1,10 +1,9 @@ use std::{env, io, path::{Path, PathBuf}, str::FromStr}; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; use yazi_fs::Xdg; -use yazi_macro::ok_or_not_found; use yazi_shared::BytesExt; #[derive(Clone, Default)] @@ -65,8 +64,11 @@ impl Dependency { Ok(()) } - pub(super) async fn plugin_files(dir: &Path) -> io::Result> { - let mut it = ok_or_not_found!(tokio::fs::read_dir(dir).await, return Ok(vec![])); + pub(super) async fn plugin_files(dir: &Path) -> Result> { + let mut it = tokio::fs::read_dir(dir) + .await + .with_context(|| format!("failed to read plugin directory `{}`", dir.display()))?; + let mut files: Vec = ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); while let Some(entry) = it.next_entry().await? { diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index c1d428cc..b3070160 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -13,6 +13,8 @@ impl Dependency { self.header("Deploying package `{name}`")?; self.is_flavor = maybe_exists(&from.join("flavor.toml")).await; + let files = + if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&from).await? }; let to = self.target(); let exists = maybe_exists(&to).await; @@ -24,18 +26,21 @@ impl Dependency { self.delete_assets().await?; let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await; - let res2 = Self::deploy_sources(&from, &to, self.is_flavor).await; + let res2 = Self::deploy_sources(&from, &to, files).await; if !exists && (res2.is_err() || res1.is_err()) { self.delete_assets().await?; self.delete_sources().await?; + } else if exists && (res2.is_err() || res1.is_err()) { + self.hash = self.hash().await?; } Local::regular(&to).remove_dir_clean().await; - self.hash = self.hash().await?; res2?; res1?; + self.hash = self.hash().await?; outln!("Done!")?; + Ok(()) } @@ -56,8 +61,7 @@ impl Dependency { Ok(()) } - async fn deploy_sources(from: &Path, to: &Path, is_flavor: bool) -> Result<()> { - let files = if is_flavor { Self::flavor_files() } else { Self::plugin_files(from).await? }; + async fn deploy_sources(from: &Path, to: &Path, files: Vec) -> Result<()> { for file in files { let (from, to) = (from.join(&file), to.join(&file)); copy_and_seal(&from, &to) From 52640fcecea337d2a17307201f71fd4a7c7a61b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Fri, 17 Jul 2026 03:04:19 +0800 Subject: [PATCH 08/12] feat: new `hub` VFS kind (#4131) --- yazi-actor/src/cmp/show.rs | 4 +- yazi-actor/src/cmp/trigger.rs | 4 +- yazi-actor/src/lives/file.rs | 12 +- yazi-actor/src/mgr/cd.rs | 4 +- yazi-actor/src/mgr/create.rs | 8 +- yazi-actor/src/mgr/filter_do.rs | 4 +- yazi-actor/src/mgr/hidden.rs | 4 +- yazi-actor/src/mgr/hover.rs | 12 +- yazi-actor/src/mgr/rename.rs | 16 +- yazi-actor/src/mgr/reveal.rs | 4 +- yazi-actor/src/mgr/sort.rs | 4 +- yazi-actor/src/mgr/update_files.rs | 10 +- yazi-actor/src/tasks/update_succeed.rs | 4 +- yazi-config/src/popup/input.rs | 2 +- yazi-config/src/vfs/authorities.rs | 20 ++- yazi-config/src/vfs/domains.rs | 76 +++++++--- yazi-config/src/vfs/service.rs | 8 +- yazi-config/src/vfs/vfs.rs | 6 +- yazi-core/src/mgr/yanked.rs | 2 +- yazi-core/src/tab/finder.rs | 8 +- yazi-core/src/tab/folder.rs | 24 +-- yazi-core/src/tab/selected.rs | 8 +- yazi-core/src/tab/tab.rs | 2 +- yazi-core/src/tasks/prework.rs | 6 +- yazi-fs/src/engine/local/absolute.rs | 2 +- yazi-fs/src/engine/local/local.rs | 22 +-- yazi-fs/src/engine/traits.rs | 10 +- yazi-fs/src/entries.rs | 103 +++++-------- yazi-fs/src/file/file.rs | 3 + yazi-fs/src/fns.rs | 2 + yazi-fs/src/op.rs | 65 ++++---- yazi-fs/src/path/clean.rs | 77 ++++++++-- yazi-fs/src/path/expand.rs | 6 + yazi-fs/src/path/percent.rs | 12 +- yazi-fs/src/sorter.rs | 4 +- yazi-fs/src/spec.rs | 16 +- yazi-fs/src/url.rs | 10 +- yazi-parser/src/mgr/hover.rs | 4 +- yazi-scheduler/src/file/in.rs | 2 +- yazi-scheduler/src/scheduler.rs | 4 +- yazi-scheduler/src/size/size.rs | 3 +- yazi-shared/src/auth/auth.rs | 128 +++++++++++++--- yazi-shared/src/auth/domain.rs | 69 +++++++++ yazi-shared/src/auth/encode.rs | 41 ++++- yazi-shared/src/auth/inventory.rs | 4 +- yazi-shared/src/auth/kind.rs | 7 +- yazi-shared/src/auth/mod.rs | 2 +- yazi-shared/src/loc/able.rs | 10 +- yazi-shared/src/loc/buf.rs | 26 ++-- yazi-shared/src/loc/loc.rs | 44 ++++-- yazi-shared/src/path/buf.rs | 20 +-- yazi-shared/src/path/component.rs | 3 + yazi-shared/src/path/components.rs | 10 ++ yazi-shared/src/path/conversion.rs | 58 -------- yazi-shared/src/path/cow.rs | 10 +- yazi-shared/src/path/dyn_path.rs | 66 +++++++++ yazi-shared/src/path/kind.rs | 1 + yazi-shared/src/path/like.rs | 58 ++++---- yazi-shared/src/path/mod.rs | 2 +- yazi-shared/src/path/path.rs | 49 +++++- yazi-shared/src/path/view.rs | 22 +-- yazi-shared/src/spec/encode.rs | 17 ++- yazi-shared/src/spec/spec.rs | 63 ++++++-- yazi-shared/src/strand/kind.rs | 1 + yazi-shared/src/tests.rs | 3 +- yazi-shared/src/url/buf.rs | 197 ++++++++++++++++++++++--- yazi-shared/src/url/components.rs | 9 +- yazi-shared/src/url/cow.rs | 31 +++- yazi-shared/src/url/de.rs | 6 +- yazi-shared/src/url/display.rs | 4 +- yazi-shared/src/url/encode.rs | 24 ++- yazi-shared/src/url/like.rs | 10 +- yazi-shared/src/url/lua.rs | 3 + yazi-shared/src/url/traits.rs | 2 + yazi-shared/src/url/url.rs | 115 ++++++++++++--- yazi-vfs/src/engine/demand.rs | 2 +- yazi-vfs/src/engine/engine.rs | 12 +- yazi-vfs/src/engine/engines.rs | 12 +- yazi-vfs/src/engine/lua/lua.rs | 20 +-- yazi-vfs/src/engine/sftp/sftp.rs | 18 +-- yazi-watcher/src/local/local.rs | 8 +- yazi-watcher/src/remote/remote.rs | 6 +- yazi-watcher/src/reporter.rs | 2 +- yazi-watcher/src/watched.rs | 2 +- 84 files changed, 1247 insertions(+), 547 deletions(-) create mode 100644 yazi-shared/src/auth/domain.rs delete mode 100644 yazi-shared/src/path/conversion.rs create mode 100644 yazi-shared/src/path/dyn_path.rs diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index a29c98db..dba4420c 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -4,7 +4,7 @@ use anyhow::Result; use yazi_core::cmp::CmpItem; use yazi_macro::{render, succ}; use yazi_parser::cmp::ShowForm; -use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; +use yazi_shared::{data::Data, path::{DynPath, PathDyn}, strand::StrandLike}; use crate::{Actor, Ctx}; @@ -30,7 +30,7 @@ impl Actor for Show { succ!(); }; - cmp.matches = Self::match_candidates(opt.word.as_path(), cache); + cmp.matches = Self::match_candidates(opt.word.dyn_path(), cache); if cmp.matches.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index f53c8cba..e67aad65 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -6,7 +6,7 @@ use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::TriggerForm; use yazi_proxy::CmpProxy; -use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{DynPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::engine; use crate::{Actor, Ctx}; @@ -78,7 +78,7 @@ impl Trigger { } // Child - let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_path(), |(_, c)| c).to_owned(); + let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.dyn_path(), |(_, c)| c).to_owned(); // Parent let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?; diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index 8a283bed..a3fbb0b0 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods}; use yazi_binding::{Range, style::Style}; use yazi_config::THEME; use yazi_fs::file::FileInventory; -use yazi_shared::{path::AsPath, url::UrlLike}; +use yazi_shared::{path::DynPath, url::UrlLike}; use super::{FILE_CACHE, Lives}; use crate::lives::{CoreRef, PtrCell}; @@ -73,7 +73,11 @@ impl UserData for File { Ok(yazi_config::THEME.icon.matches(me, me.is_hovered())) }); methods.add_method("size", |_, me, ()| { - Ok(if me.is_dir() { me.folder.entries.sizes.get(&me.urn()).copied() } else { Some(me.len) }) + Ok(if me.is_dir() { + me.folder.entries.sizes.get(&me.entry_key()).copied() + } else { + Some(me.len) + }) }); methods.add_method("mime", |lua, me, ()| { let core: CoreRef = lua.named_registry_value("cx")?; @@ -86,7 +90,7 @@ impl UserData for File { let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); comp.next_back(); - Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() + Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose() }); methods.add_method("style", |lua, me, ()| { let core: CoreRef = lua.named_registry_value("cx")?; @@ -122,7 +126,7 @@ impl UserData for File { return Ok(None); }; - let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else { + let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else { return Ok(None); }; diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index ab852086..2e6b9efc 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -81,8 +81,8 @@ impl Cd { return MgrProxy::cd(&url, CdSource::Cd); } - if let Some(p) = url.parent() { - FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); + if let Some((p, k)) = url.pair2() { + FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit(); } MgrProxy::reveal(url); } diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 0613a212..3bd14da7 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -64,10 +64,10 @@ impl Create { if dir { engine::create_dir_all(&new).await?; } else if let Ok(real) = engine::casefold(&new).await - && let Some((parent, urn)) = real.pair() + && let Some((parent, key)) = real.pair2() { ok_or_not_found!(engine::remove_file(&new).await); - FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); + FilesOp::Deleting(parent.into(), [key.into()].into()).emit(); engine::create(&new).await?; } else if let Some(parent) = new.parent() { engine::create_dir_all(parent).await.ok(); @@ -78,10 +78,10 @@ impl Create { } if let Ok(real) = engine::casefold(&new).await - && let Some((parent, urn)) = real.pair() + && let Some((parent, key)) = real.pair2() { let file = File::new(&real).await?; - FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); + FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit(); MgrProxy::reveal(&real); } diff --git a/yazi-actor/src/mgr/filter_do.rs b/yazi-actor/src/mgr/filter_do.rs index 0d76cf11..a995c65f 100644 --- a/yazi-actor/src/mgr/filter_do.rs +++ b/yazi-actor/src/mgr/filter_do.rs @@ -16,10 +16,10 @@ impl Actor for FilterDo { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result { let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; - let hovered = cx.hovered().map(|f| f.urn().into()); + let hovered = cx.hovered().map(|f| f.entry_key().into()); cx.current_mut().entries.set_filter(filter); - if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) { act!(mgr:hover, cx, hovered)?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 6113805a..02cb0ad5 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -18,7 +18,7 @@ impl Actor for Hidden { let state = form.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; - let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -43,7 +43,7 @@ impl Actor for Hidden { { render!(h.repos(None)); act!(mgr:peek, cx, true)?; - } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) { act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index b702d837..4cdfcebb 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -18,19 +18,19 @@ impl Actor for Hover { // Parent should always track CWD if let Some(p) = &mut tab.parent { - render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); + render!(p.repos(Some(tab.current.url.entry_key()))); } // Repos CWD - render!(tab.current.repos(form.urn.as_ref().map(Into::into))); + render!(tab.current.repos(form.key.as_ref().map(Into::into))); // Turn on tracing - if let (Some(h), Some(u)) = (tab.hovered(), form.urn) - && h.urn() == u + if let (Some(h), Some(key)) = (tab.hovered(), form.key) + && h.entry_key() == key { // `hover(Some)` occurs after user actions, such as create, rename, reveal, etc. - // At this point, it's intuitive to track the file location regardless. - tab.current.trace = Some(u.clone()); + // At this point, it's intuitive to track the entry regardless. + tab.current.trace = Some(key.clone()); cx.tasks.scheduler.behavior.reset(); } diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 423640de..031134fe 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -66,8 +66,8 @@ impl Actor for Rename { impl Rename { async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> { - let Some((old_p, old_n)) = old.pair() else { return Ok(()) }; - let Some(_) = new.pair() else { return Ok(()) }; + let Some((old_p, old_k)) = old.pair2() else { return Ok(()) }; + let Some(_) = new.pair2() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); let overwritten = engine::casefold(&new).await; @@ -75,21 +75,21 @@ impl Rename { if let Ok(u) = overwritten && u != new - && let Some((parent, urn)) = u.pair() + && let Some((parent, key)) = u.pair2() { ok_or_not_found!(engine::rename(&u, &new).await); - FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); + FilesOp::Deleting(parent.to_owned(), [key.into()].into()).emit(); } let new = engine::casefold(&new).await?; - let Some((new_p, new_n)) = new.pair() else { return Ok(()) }; + let Some((new_p, new_k)) = new.pair2() else { return Ok(()) }; let file = File::new(&new).await?; if new_p == old_p { - FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit(); + FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit(); } else { - FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit(); - FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit(); + FilesOp::Deleting(old_p.into(), [old_k.into()].into()).emit(); + FilesOp::Upserting(new_p.into(), [(new_k.into(), file)].into()).emit(); } MgrProxy::reveal(&new); diff --git a/yazi-actor/src/mgr/reveal.rs b/yazi-actor/src/mgr/reveal.rs index e7133d06..ad197364 100644 --- a/yazi-actor/src/mgr/reveal.rs +++ b/yazi-actor/src/mgr/reveal.rs @@ -14,7 +14,7 @@ impl Actor for Reveal { const NAME: &str = "reveal"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - let Some((parent, child)) = form.target.pair() else { succ!() }; + let Some((parent, child)) = form.target.pair2() else { succ!() }; // Cd to the parent directory act!(mgr:cd, cx, (parent, form.source))?; @@ -25,7 +25,7 @@ impl Actor for Reveal { // If the child is not hovered, which means it doesn't exist, // create a dummy file - if !form.no_dummy && tab.hovered().is_none_or(|f| child != f.urn()) { + if !form.no_dummy && tab.hovered().is_none_or(|f| f.entry_key() != child) { let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&form.target, None)]); tab.current.update_pub(tab.id, op); } diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index 270e2067..dd94b4f8 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -24,7 +24,7 @@ impl Actor for Sort { pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback); let sorter = FilesSorter::from(&*pref); - let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -50,7 +50,7 @@ impl Actor for Sort { { render!(h.repos(None)); act!(mgr:peek, cx, true)?; - } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) { act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index ff4a7b3a..94ceb1a1 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -57,12 +57,12 @@ impl UpdateFiles { fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result { let tab = cx.tab_mut(); - let urn = tab.current.url.urn(); - let leave = matches!(op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn)); + let key = tab.current.url.entry_key(); + let leave = matches!(op, FilesOp::Deleting(_, ref keys) if keys.contains(&key)); if let Some(f) = tab.parent.as_mut() { render!(f.update_pub(tab.id, op)); - render!(f.hover(urn)); + render!(f.hover(key)); } if leave { @@ -97,8 +97,8 @@ impl UpdateFiles { fn update_history(cx: &mut Ctx, op: FilesOp) -> Result { let tab = &mut cx.tab_mut(); - let leave = tab.parent.as_ref().and_then(|f| f.url.parent().map(|p| (p, f.url.urn()))).is_some_and( - |(p, n)| matches!(op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(&n)), + let leave = tab.parent.as_ref().and_then(|f| f.url.pair2()).is_some_and( + |(pp, key)| matches!(&op, FilesOp::Deleting(parent, keys) if parent == pp && keys.contains(&key)), ); tab.history.get_or_insert_with(op.cwd(), |u| Folder::from(u)).update_pub(tab.id, op); diff --git a/yazi-actor/src/tasks/update_succeed.rs b/yazi-actor/src/tasks/update_succeed.rs index 0b286951..7d0186a5 100644 --- a/yazi-actor/src/tasks/update_succeed.rs +++ b/yazi-actor/src/tasks/update_succeed.rs @@ -19,10 +19,10 @@ impl Actor for UpdateSucceed { if form.track && form.id == cx.tasks.scheduler.behavior.first_id() - && let Some((parent, urn)) = form.urls[0].pair() + && let Some((parent, key)) = form.urls[0].pair2() && parent == *cx.cwd() { - cx.current_mut().trace = Some(urn.into()); + cx.current_mut().trace = Some(key.into()); act!(mgr:hover, cx)?; } diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index bb4d1659..33bde408 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use yazi_binding::position::{Offset, Origin, Position}; use yazi_codegen::{DeserializeOver, DeserializeOver2}; -use yazi_shared::{spec::Encode as EncodeSpec, url::Url}; +use yazi_shared::{spec::EncodeSpec, url::Url}; use yazi_widgets::input::InputOpt; #[derive(Deserialize, DeserializeOver, DeserializeOver2)] diff --git a/yazi-config/src/vfs/authorities.rs b/yazi-config/src/vfs/authorities.rs index 23ba0a19..e7899798 100644 --- a/yazi-config/src/vfs/authorities.rs +++ b/yazi-config/src/vfs/authorities.rs @@ -1,16 +1,28 @@ +use std::sync::Arc; + use hashbrown::HashMap; use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}}; -use yazi_shared::auth::Scheme; +use yazi_shared::auth::{Auth, Domain, Scheme}; use yazi_shim::toml::DeserializeOverWith; -use super::{DomainSeed, Domains, Service}; +use super::{DomainSeed, Domains}; +use crate::vfs::Service; pub struct Authorities(HashMap); impl Authorities { - pub fn get(&self, scheme: &Scheme, domain: &str) -> Option<&Service> { + pub fn service(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option<&Service> { self.0.get(scheme)?.get(domain) } + + pub fn auth(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option> { + let service = self.service(scheme, domain)?; + if service.auth().domain.is_catchall() { + Some(Auth::new(service.kind(), scheme.clone(), domain.clone())) + } else { + Some(service.auth().clone()) + } + } } impl<'de> Deserialize<'de> for Authorities { @@ -41,7 +53,7 @@ impl<'de> Deserialize<'de> for Authorities { impl DeserializeOverWith for Authorities { fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result { for (scheme, domains) in Self::deserialize(de)?.0 { - self.0.entry(scheme).or_default().extend(domains.0); + self.0.entry(scheme).or_default().extend(domains); } Ok(self) } diff --git a/yazi-config/src/vfs/domains.rs b/yazi-config/src/vfs/domains.rs index 665e07d3..d6f81b77 100644 --- a/yazi-config/src/vfs/domains.rs +++ b/yazi-config/src/vfs/domains.rs @@ -1,34 +1,66 @@ -use std::{ops::{Deref, DerefMut}, sync::Arc}; +use std::sync::Arc; use hashbrown::HashMap; -use serde::{Deserialize, Deserializer, de::{DeserializeSeed, Error}}; -use yazi_shared::{KebabCasedKey, auth::Scheme}; +use serde::{Deserialize, Deserializer, de::{self, DeserializeSeed, Error}}; +use yazi_shared::auth::{AuthKind, Domain, Scheme}; use super::{Service, ServiceSftp}; #[derive(Default)] -pub struct Domains(pub(super) HashMap); - -impl Deref for Domains { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Domains { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +pub struct Domains { + exact: HashMap, Service>, + catchall: Option, } impl Domains { + pub fn get(&self, domain: &Domain<'_>) -> Option<&Service> { + self.exact.get(domain.as_ref()).or(self.catchall.as_ref()) + } + + pub fn extend(&mut self, other: Self) { + self.exact.extend(other.exact); + if other.catchall.is_some() { + self.catchall = other.catchall; + } + } + fn init(&mut self, scheme: &Scheme) { - for (domain, service) in &mut self.0 { + for (domain, service) in &mut self.exact { let kind = service.kind(); let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); auth.kind = kind; auth.scheme = scheme.clone(); - auth.domain = domain.clone().into(); + auth.domain = domain.clone(); } + + if let Some(service) = &mut self.catchall { + let kind = service.kind(); + let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); + + auth.kind = kind; + auth.scheme = scheme.clone(); + auth.domain = Domain::CATCHALL; + } + } + + fn from_map(map: HashMap, Service>) -> Result + where + E: de::Error, + { + let mut domains = Self::default(); + for (domain, service) in map { + if domain.is_catchall() { + domains.catchall = Some(service); + continue; + } + + if service.kind() == AuthKind::Hub { + return Err(E::custom("Hub services require a `*` catch-all domain")); + } + domains.exact.insert(domain, service); + } + Ok(domains) } } @@ -39,22 +71,24 @@ impl<'de> DeserializeSeed<'de> for DomainSeed<'_> { type Value = Domains; fn deserialize>(self, deserializer: D) -> Result { - let mut domains = Domains(match self.0 { + let mut domains = match self.0 { Scheme::Regular | Scheme::Search => { return Err(D::Error::custom("scheme cannot be configured")); } Scheme::Sftp => { - let map = HashMap::::deserialize(deserializer)?; - map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect() + let map = HashMap::, ServiceSftp>::deserialize(deserializer)?; + Domains::from_map( + map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect(), + )? } Scheme::Custom(_) => { - let map = HashMap::::deserialize(deserializer)?; + let map = HashMap::, Service>::deserialize(deserializer)?; if map.values().any(|service| matches!(service, Service::Sftp(_))) { return Err(D::Error::custom("SFTP services must use the `sftp` scheme")); } - map + Domains::from_map(map)? } - }); + }; domains.init(self.0); Ok(domains) diff --git a/yazi-config/src/vfs/service.rs b/yazi-config/src/vfs/service.rs index 9c5604fa..4e47e49e 100644 --- a/yazi-config/src/vfs/service.rs +++ b/yazi-config/src/vfs/service.rs @@ -10,6 +10,7 @@ use super::{ServiceLua, ServiceSftp}; pub enum Service { Sftp(ServiceSftp), Mount(ServiceLua), + Hub(ServiceLua), Scope(ServiceLua), } @@ -19,7 +20,7 @@ impl TryFrom<&'static Service> for &'static ServiceSftp { fn try_from(value: &'static Service) -> Result { match value { Service::Sftp(p) => Ok(p), - Service::Mount(_) | Service::Scope(_) => { + Service::Mount(_) | Service::Hub(_) | Service::Scope(_) => { Err("expected an SFTP service, got a custom VFS service") } } @@ -32,7 +33,7 @@ impl TryFrom<&'static Service> for &'static ServiceLua { fn try_from(value: &'static Service) -> Result { match value { Service::Sftp(_) => Err("expected a custom VFS service, got an SFTP service"), - Service::Mount(lua) | Service::Scope(lua) => Ok(lua), + Service::Mount(lua) | Service::Hub(lua) | Service::Scope(lua) => Ok(lua), } } } @@ -42,6 +43,7 @@ impl Service { match self { Self::Sftp(_) => AuthKind::Sftp, Self::Mount(_) => AuthKind::Mount, + Self::Hub(_) => AuthKind::Hub, Self::Scope(_) => AuthKind::Scope, } } @@ -50,6 +52,7 @@ impl Service { match self { Self::Sftp(sftp) => &sftp.auth, Self::Mount(lua) => &lua.auth, + Self::Hub(lua) => &lua.auth, Self::Scope(lua) => &lua.auth, } } @@ -58,6 +61,7 @@ impl Service { match self { Self::Sftp(sftp) => &mut sftp.auth, Self::Mount(lua) => &mut lua.auth, + Self::Hub(lua) => &mut lua.auth, Self::Scope(lua) => &mut lua.auth, } } diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 6ed4ca24..902b8357 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -21,7 +21,7 @@ impl Vfs { where P: TryFrom<&'static Service, Error = &'static str>, { - let Some(value) = VFS.authorities.get(&auth.scheme, &auth.domain) else { + let Some(value) = VFS.authorities.service(&auth.scheme, &auth.domain) else { return Err(io::Error::other(format!("No such VFS service: {auth}"))); }; @@ -47,8 +47,6 @@ impl DeserializeOverWith for Vfs { // --- Inject inventory::submit! { AuthInventory { - get: |scheme, domain| { - VFS.authorities.get(scheme, domain).map(|service| service.auth().clone()) - }, + get: |scheme, domain| VFS.authorities.auth(scheme, domain), } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index babb92be..008189a6 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -67,7 +67,7 @@ impl Yanked { } pub fn apply_op(&mut self, op: &FilesOp) { - let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); + let (removal, addition) = op.diff_recoverable(self.urls()); if !removal.is_empty() { let old = self.files.len(); self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u))); diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index fa8111e0..0594ba97 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -1,7 +1,7 @@ use anyhow::Result; use hashbrown::HashMap; use yazi_fs::{Entries, Filter, FilterCase}; -use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; +use yazi_shared::{path::{DynPath, PathBufDyn}, url::UrlBuf}; use crate::tab::Folder; @@ -62,7 +62,7 @@ impl Finder { continue; } - self.matched.insert(file.urn().into(), i); + self.matched.insert(file.entry_key().into(), i); if self.matched.len() > 99 { break; } @@ -78,9 +78,9 @@ impl Finder { impl Finder { pub fn matched_idx(&self, folder: &Folder, urn: T) -> Option where - T: AsPath, + T: DynPath, { - if self.lock == *folder { self.matched.get(&urn.as_path()).copied() } else { None } + if self.lock == *folder { self.matched.get(&urn.dyn_path()).copied() } else { None } } } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 60f68565..e9b75d49 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -4,7 +4,7 @@ use yazi_config::{LAYOUT, YAZI}; use yazi_dds::Pubsub; use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File}; use yazi_macro::err; -use yazi_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; +use yazi_shared::{id::Id, path::{DynPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::{Scrollable, Step}; use crate::MgrProxy; @@ -104,24 +104,26 @@ impl Folder { b } - pub fn hover(&mut self, urn: PathDyn) -> bool { - if self.hovered().map(|h| h.urn()) == Some(urn) { + pub fn hover(&mut self, key: PathDyn) -> bool { + if key.is_empty() { + return self.arrow(0); + } else if self.hovered().map(|h| h.entry_key()) == Some(key) { return self.arrow(0); } - let new = self.entries.position(urn).unwrap_or(self.cursor) as isize; + let new = self.entries.position(key).unwrap_or(self.cursor) as isize; let b = self.arrow(new - self.cursor as isize); self.retrace(); b } - pub fn repos(&mut self, urn: Option) -> bool { - if let Some(u) = urn { - self.hover(u) - } else if let Some(u) = self.trace.take() { - let b = self.hover(u.as_path()); - self.trace = Some(u); + pub fn repos(&mut self, key: Option) -> bool { + if let Some(k) = key { + self.hover(k) + } else if let Some(k) = self.trace.take() { + let b = self.hover(k.dyn_path()); + self.trace = Some(k); b } else { self.arrow(0) @@ -129,7 +131,7 @@ impl Folder { } pub fn retrace(&mut self) { - self.trace = self.hovered().map(|h| h.urn().into()).or(self.trace.take()); + self.trace = self.hovered().map(|h| h.entry_key().into()).or(self.trace.take()); } pub fn sync_page(&mut self, force: bool) { diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 5805d022..c41fab77 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -133,7 +133,7 @@ impl Selected { } pub fn apply_op(&mut self, op: &FilesOp) { - let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); + let (removal, addition) = op.diff_recoverable(self.urls()); if !removal.is_empty() { self.remove_many(&removal); } @@ -190,6 +190,12 @@ mod tests { assert!(!s.remove(Path::new("/a/b"))); assert!(s.inner.is_empty()); assert!(s.parents.is_empty()); + + // Relative path with an empty parent (root) + assert!(s.add(f("a"))); + assert!(s.remove(Path::new("a"))); + assert!(s.inner.is_empty()); + assert!(s.parents.is_empty()); } #[test] diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 958ba927..61ebd7c0 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -85,7 +85,7 @@ impl Tab { pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } pub fn hovered_rect(&self) -> Option { - let y = self.current.entries.position(self.hovered()?.urn())? - self.current.offset; + let y = self.current.entries.position(self.hovered()?.entry_key())? - self.current.offset; let mut rect = LAYOUT.get().current; rect.y = rect.y.saturating_sub(1) + y as u16; diff --git a/yazi-core/src/tasks/prework.rs b/yazi-core/src/tasks/prework.rs index be641480..23ec2ca5 100644 --- a/yazi-core/src/tasks/prework.rs +++ b/yazi-core/src/tasks/prework.rs @@ -54,7 +54,11 @@ impl Tasks { targets .iter() .filter(|f| { - f.is_dir() && !targets.sizes.contains_key(&f.urn()) && !loading.contains(&f.url) + let key = f.entry_key(); + f.is_dir() + && !key.is_empty() + && !targets.sizes.contains_key(&key) + && !loading.contains(&f.url) }) .map(|f| &f.url) .collect() diff --git a/yazi-fs/src/engine/local/absolute.rs b/yazi-fs/src/engine/local/absolute.rs index 091c2d84..4f362fb4 100644 --- a/yazi-fs/src/engine/local/absolute.rs +++ b/yazi-fs/src/engine/local/absolute.rs @@ -51,6 +51,6 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option> { Some(match url.as_url() { Url::Regular(_) => UrlBuf::Regular(loc).into(), Url::Search { auth, .. } => UrlBuf::Search { loc, auth: auth.clone() }.into(), - Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?, + Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?, }) } diff --git a/yazi-fs/src/engine/local/local.rs b/yazi-fs/src/engine/local/local.rs index 32e476d3..7f1dcc63 100644 --- a/yazi-fs/src/engine/local/local.rs +++ b/yazi-fs/src/engine/local/local.rs @@ -1,7 +1,7 @@ use std::{fs::FileTimes, io, path::Path, sync::Arc}; use tokio::sync::mpsc; -use yazi_shared::{auth::AuthKind, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{auth::AuthKind, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; use crate::{cha::{Cha, ChaMode}, engine::{Attrs, Capabilities, Engine}}; @@ -45,19 +45,19 @@ impl<'a> Engine for Local<'a> { #[inline] async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPath, + P: DynPath, { - let to = to.as_path().to_os_owned()?; + let to = to.dyn_path().to_os_owned()?; let from = self.path.to_owned(); super::copy_impl(from, to, attrs).await } fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { - let to = to.as_path().to_os_owned()?; + let to = to.dyn_path().to_os_owned()?; let from = self.path.to_owned(); Ok(super::copy_progressive_impl(from, to, attrs.into())) } @@ -71,9 +71,9 @@ impl<'a> Engine for Local<'a> { #[inline] async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_os()?; + let to = to.dyn_path().as_os()?; tokio::fs::hard_link(self.path, to).await } @@ -87,7 +87,7 @@ impl<'a> Engine for Local<'a> { async fn new<'b>(url: Url<'b>) -> io::Result> { match url { Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }), - Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => { + Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => { Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) } } @@ -101,7 +101,7 @@ impl<'a> Engine for Local<'a> { reader: tokio::fs::read_dir(self.path).await?, dir: Arc::new(self.url.to_owned()), }, - AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new( + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Not a local URL: {:?}", self.url), ))?, @@ -125,9 +125,9 @@ impl<'a> Engine for Local<'a> { #[inline] async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_os()?; + let to = to.dyn_path().as_os()?; tokio::fs::rename(self.path, to).await } diff --git a/yazi-fs/src/engine/traits.rs b/yazi-fs/src/engine/traits.rs index c26ee590..6f42e63e 100644 --- a/yazi-fs/src/engine/traits.rs +++ b/yazi-fs/src/engine/traits.rs @@ -2,7 +2,7 @@ use std::io; use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; -use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; +use yazi_shared::{path::{DynPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}}; @@ -23,11 +23,11 @@ pub trait Engine: Sized { fn copy

(&self, to: P, attrs: Attrs) -> impl Future> where - P: AsPath; + P: DynPath; fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into; fn create(&self) -> impl Future> { @@ -80,7 +80,7 @@ pub trait Engine: Sized { fn hard_link

(&self, to: P) -> impl Future> where - P: AsPath; + P: DynPath; fn metadata(&self) -> impl Future>; @@ -157,7 +157,7 @@ pub trait Engine: Sized { fn rename

(&self, to: P) -> impl Future> where - P: AsPath; + P: DynPath; fn set_attrs(&self, attrs: Attrs) -> impl Future>; diff --git a/yazi-fs/src/entries.rs b/yazi-fs/src/entries.rs index 84c7c426..86ac0796 100644 --- a/yazi-fs/src/entries.rs +++ b/yazi-fs/src/entries.rs @@ -1,7 +1,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; -use yazi_shared::{id::Id, path::{PathBufDyn, PathDyn}}; +use yazi_shared::{id::Id, path::{PathBufDyn, PathDyn, PathLike}}; use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, SortBy, file::File}; @@ -69,19 +69,19 @@ impl Entries { } } - pub fn update_size(&mut self, mut sizes: HashMap) { - if sizes.len() <= 50 { - sizes.retain(|k, v| self.sizes.get(k) != Some(v)); + pub fn update_size(&mut self, sizes: HashMap) { + self.sizes.reserve(if self.sizes.is_empty() { sizes.len() } else { sizes.len().div_ceil(2) }); + + let mut changed = false; + for (key, size) in sizes { + if !key.is_empty() { + changed |= self.sizes.insert(key, size) != Some(size); + } } - if sizes.is_empty() { - return; - } - - if self.sorter.by == SortBy::Size { + if changed && self.sorter.by == SortBy::Size { self.revision += 1; } - self.sizes.extend(sizes); } pub fn update_ioerr(&mut self) { @@ -97,9 +97,10 @@ impl Entries { macro_rules! go { ($dist:expr, $src:expr, $inc:literal) => { - let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().to_owned(), f)).collect(); + let mut todo: HashMap<_, _> = + $src.into_iter().map(|f| (f.entry_key().to_owned(), f)).collect(); for f in &$dist { - if todo.remove(&f.urn()).is_some() && todo.is_empty() { + if todo.remove(&f.entry_key()).is_some() && todo.is_empty() { break; } } @@ -119,48 +120,14 @@ impl Entries { } } - #[cfg(unix)] - pub fn update_deleting(&mut self, urns: HashSet) -> Vec { - use yazi_shared::path::PathLike; - if urns.is_empty() { - return vec![]; - } + pub fn update_deleting(&mut self, mut keys: HashSet) -> Vec { + keys.retain(|k| !k.is_empty()); + let mut deleted = Vec::with_capacity(keys.len()); - let (mut hidden, mut items) = if let Some(filter) = &self.filter { - urns.into_iter().partition(|u| (!self.show_hidden && u.is_hidden()) || !filter.matches(u)) - } else if self.show_hidden { - (HashSet::new(), urns) - } else { - urns.into_iter().partition(|u| u.is_hidden()) - }; - - let mut deleted = Vec::with_capacity(items.len()); - if !items.is_empty() { + if !keys.is_empty() { let mut i = 0; self.items.retain(|f| { - let b = items.remove(&f.urn()); - if b { - deleted.push(i); - } - i += 1; - !b - }); - } - if !hidden.is_empty() { - self.hidden.retain(|f| !hidden.remove(&f.urn())); - } - - self.revision += deleted.is_empty().not() as u64; - deleted - } - - #[cfg(windows)] - pub fn update_deleting(&mut self, mut urns: HashSet) -> Vec { - let mut deleted = Vec::with_capacity(urns.len()); - if !urns.is_empty() { - let mut i = 0; - self.items.retain(|f| { - let b = urns.remove(&f.urn()); + let b = keys.remove(&f.entry_key()); if b { deleted.push(i) } @@ -168,8 +135,9 @@ impl Entries { !b }); } - if !urns.is_empty() { - self.hidden.retain(|f| !urns.remove(&f.urn())); + + if !keys.is_empty() { + self.hidden.retain(|f| !keys.remove(&f.entry_key())); } self.revision += deleted.is_empty().not() as u64; @@ -178,8 +146,9 @@ impl Entries { pub fn update_updating( &mut self, - files: HashMap, + mut files: HashMap, ) -> (HashMap, HashMap) { + files.retain(|k, f| !k.is_empty() && !f.entry_key().is_empty()); if files.is_empty() { return Default::default(); } @@ -188,9 +157,9 @@ impl Entries { ($dist:expr, $src:expr, $inc:literal) => { let mut b = true; for i in 0..$dist.len() { - if let Some(f) = $src.remove(&$dist[i].urn()) { + if let Some(f) = $src.remove(&$dist[i].entry_key()) { b = b && $dist[i].cha.hits(f.cha); - b = b && $dist[i].urn() == f.urn(); + b = b && $dist[i].entry_key() == f.entry_key(); $dist[i] = f; if $src.is_empty() { @@ -221,13 +190,18 @@ impl Entries { (hidden, items) } - pub fn update_upserting(&mut self, files: HashMap) { + pub fn update_upserting(&mut self, mut files: HashMap) { + files.retain(|k, f| !k.is_empty() && !f.entry_key().is_empty()); if files.is_empty() { return; } self.update_deleting( - files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().into()).collect(), + files + .iter() + .filter(|&(k, f)| k != f.entry_key()) + .map(|(_, f)| f.entry_key().into()) + .collect(), ); let (hidden, items) = self.update_updating(files); @@ -255,14 +229,13 @@ impl Entries { } fn split_files(&self, files: impl IntoIterator) -> (Vec, Vec) { + let files = files.into_iter().filter(|f| !f.entry_key().is_empty()); if let Some(filter) = &self.filter { - files - .into_iter() - .partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn())) + files.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn())) } else if self.show_hidden { - (vec![], files.into_iter().collect()) + (vec![], files.collect()) } else { - files.into_iter().partition(|f| f.is_hidden()) + files.partition(|f| f.is_hidden()) } } } @@ -270,7 +243,9 @@ impl Entries { impl Entries { // --- Items #[inline] - pub fn position(&self, urn: PathDyn) -> Option { self.iter().position(|f| urn == f.urn()) } + pub fn position(&self, key: PathDyn) -> Option { + if key.is_empty() { None } else { self.iter().position(|f| f.entry_key() == key) } + } // --- Ticket #[inline] diff --git a/yazi-fs/src/file/file.rs b/yazi-fs/src/file/file.rs index 1318dbf1..58a97779 100644 --- a/yazi-fs/src/file/file.rs +++ b/yazi-fs/src/file/file.rs @@ -63,6 +63,9 @@ impl File { #[inline] pub fn urn(&self) -> PathDyn<'_> { self.url.urn() } + #[inline] + pub fn entry_key(&self) -> PathDyn<'_> { self.url.entry_key() } + #[inline] pub fn name(&self) -> Option> { self.url.name() } diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index 61239627..4269d736 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -70,6 +70,8 @@ fn test_max_common_root() { assert_eq!(max_common_root(&[]), 0); assert(&[""], ""); assert(&["a"], ""); + assert(&["search://kw:1:1/a", "search://kw:1:1/b"], "search://kw/"); + assert(&["test-hub://a1/@root/a", "test-hub://b1/@root/b"], "test-hub://root/@/"); assert(&["/a"], "/"); assert(&["/a/b"], "/a"); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index 421f7452..e75733b6 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -2,7 +2,7 @@ use std::{iter, path::Path}; use hashbrown::{HashMap, HashSet}; use yazi_macro::{impl_data_any, relay}; -use yazi_shared::{id::{Id, Ids}, path::PathBufDyn, url::{UrlBuf, UrlLike, UrlMapExt}}; +use yazi_shared::{id::{Id, Ids}, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike, UrlMapExt}}; use crate::{cha::Cha, file::File}; @@ -42,16 +42,17 @@ impl FilesOp { pub fn files(&self) -> Box + '_> { match self { - Self::Full(_, files, _) => Box::new(files.iter()), - Self::Part(_, files, _) => Box::new(files.iter()), + Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => { + Box::new(files.iter().filter(|f| !f.entry_key().is_empty())) + } Self::Done(..) => Box::new(iter::empty()), Self::Size(..) => Box::new(iter::empty()), Self::IOErr(..) => Box::new(iter::empty()), - Self::Creating(_, files) => Box::new(files.iter()), Self::Deleting(..) => Box::new(iter::empty()), - Self::Updating(_, map) => Box::new(map.values()), - Self::Upserting(_, map) => Box::new(map.values()), + Self::Updating(_, map) | Self::Upserting(_, map) => { + Box::new(map.values().filter(|f| !f.entry_key().is_empty())) + } } } @@ -68,7 +69,7 @@ impl FilesOp { pub fn create(files: Vec) { let mut parents: HashMap> = Default::default(); for file in files { - if let Some(p) = file.url.parent() { + if let Some((p, _)) = file.url.pair2() { parents.get_or_insert_default(p).push(file); } } @@ -80,18 +81,18 @@ impl FilesOp { pub fn rename(map: HashMap) { let mut parents: HashMap, HashMap<_, _>)> = Default::default(); for (o, n) in map { - let Some(o_p) = o.parent() else { continue }; - let Some(n_p) = n.url.parent() else { continue }; + let Some((o_p, o_k)) = o.pair2() else { continue }; + let Some((n_p, n_k)) = n.url.pair2() else { continue }; if o_p == n_p { - parents.get_or_insert_default(o_p).1.insert(o.urn().into(), n); + parents.get_or_insert_default(o_p).1.insert(o_k.into(), n); } else { - parents.get_or_insert_default(o_p).0.insert(o.urn().into()); - parents.get_or_insert_default(n_p).1.insert(n.urn().into(), n); + parents.get_or_insert_default(o_p).0.insert(o_k.into()); + parents.get_or_insert_default(n_p).1.insert(n_k.into(), n); } } for (p, (o, n)) in parents { match (o.is_empty(), n.is_empty()) { - (true, true) => unreachable!(), + (true, true) => {} (true, false) => Self::Upserting(p, n).emit(), (false, true) => Self::Deleting(p, o).emit(), (false, false) => { @@ -106,14 +107,20 @@ impl FilesOp { let mut parents: HashMap<_, (HashMap<_, _>, HashSet<_>)> = Default::default(); for op in ops { match op { - Self::Upserting(p, map) => parents.entry(p).or_default().0.extend(map), - Self::Deleting(p, urns) => parents.entry(p).or_default().1.extend(urns), + Self::Upserting(p, map) => parents + .entry(p) + .or_default() + .0 + .extend(map.into_iter().filter(|(k, f)| !k.is_empty() && !f.entry_key().is_empty())), + Self::Deleting(p, keys) => { + parents.entry(p).or_default().1.extend(keys.into_iter().filter(|k| !k.is_empty())) + } _ => unreachable!(), } } for (p, (u, d)) in parents { match (u.is_empty(), d.is_empty()) { - (true, true) => unreachable!(), + (true, true) => {} (true, false) => Self::Deleting(p, d).emit(), (false, true) => Self::Upserting(p, u).emit(), (false, false) => { @@ -147,17 +154,25 @@ impl FilesOp { } } - pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { + pub fn diff_recoverable<'a, I>(&self, urls: I) -> (Vec, Vec) + where + I: IntoIterator, + { + let cwd = self.cwd(); + let it = urls + .into_iter() + .filter(|u| u.parent().is_some_and(|p| p == *cwd)) + .filter(|u| !u.entry_key().is_empty()); + match self { - Self::Deleting(cwd, urns) => { - (urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) + Self::Deleting(_, keys) => { + (it.filter(|u| keys.contains(&u.entry_key())).cloned().collect(), vec![]) } - Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns - .iter() - .filter(|&(u, f)| u != f.urn()) - .filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) - .filter(|(u, _)| contains(u)) - .map(|(u, f)| (u, f.clone())) + Self::Updating(_, files) | Self::Upserting(_, files) => it + .filter_map(|u| files.get(&u.entry_key()).map(|f| (u, f))) + .filter(|(_, f)| !f.entry_key().is_empty()) + .filter(|&(u, f)| u != f.url) + .map(|(u, f)| (u.clone(), f.clone())) .unzip(), _ => (vec![], vec![]), } diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index 2fef639f..1ce63fac 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -1,27 +1,44 @@ -use yazi_shared::{path::{PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}}; +use std::{iter, sync::Arc}; + +use yazi_shared::{auth::{Auth, AuthKind}, path::{DynPath, PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}}; pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { let cow: UrlCow = url.into(); - let (path, uri, urn) = clean_path_impl( - cow.loc(), - cow.base().components().count() - 1, - cow.trail().components().count() - 1, + + let depth = cow.loc().components().count(); + let base = depth - cow.uri().components().count(); + let trail = depth - cow.urn().components().count(); + + let (mut spec, path) = cow.into_pair(); + let (path, uri, urn, auth) = clean_path_impl( + path.dyn_path(), + base, + trail, + (spec.kind == AuthKind::Hub).then(|| spec.auth.clone()), ); - let spec = cow.into_spec().with_ports(uri, urn); - (spec, path).try_into().expect("UrlBuf from cleaned path") + spec.auth = auth.unwrap_or(spec.auth); + (spec.with_ports(uri, urn), path).try_into().expect("UrlBuf from cleaned path") } -fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usize, usize) { +fn clean_path_impl( + path: PathDyn, + base: usize, + trail: usize, + auth: Option>, +) -> (PathBufDyn, usize, usize, Option>) { use yazi_shared::path::Component::*; + let mut auths: Vec<_> = iter::successors(auth, |auth| auth.parent.clone()).collect(); + let root = auths.pop(); + let mut out = vec![]; let mut uri_count = 0; let mut urn_count = 0; macro_rules! push { - ($i:ident, $c:ident) => {{ - out.push(($i, $c)); + ($i:ident, $c:ident, $auth:ident) => {{ + out.push(($i, $c, $auth)); if $i >= base { uri_count += 1; } @@ -33,7 +50,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi macro_rules! pop { () => {{ - if let Some((i, _)) = out.pop() { + if let Some((i, ..)) = out.pop() { if i >= base { uri_count -= 1; } @@ -45,14 +62,15 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi } for (i, c) in path.components().enumerate() { + let auth = (root.is_some() && c.has_auth()).then(|| auths.pop().unwrap()); match c { CurDir => {} - ParentDir => match out.last().map(|(_, c)| c) { + ParentDir => match out.last().map(|(_, c, _)| c) { Some(RootDir) => {} Some(Normal(_)) => pop!(), - None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c), + None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c, auth), }, - c => push!(i, c), + c => push!(i, c, auth), } } @@ -60,11 +78,18 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi let path = if out.is_empty() { PathBufDyn::with_str(kind, ".") } else { - PathBufDyn::from_components(kind, out.into_iter().map(|(_, c)| c)) + PathBufDyn::from_components(kind, out.iter().map(|(_, c, _)| *c)) .expect("components with same kind") }; - (path, uri_count, urn_count) + let auth = root.map(|mut parent| { + for mut auth in out.into_iter().filter_map(|(_, _, auth)| auth) { + Arc::make_mut(&mut auth).parent = Some(parent); + parent = auth; + } + parent + }); + (path, uri_count, urn_count, auth) } #[cfg(test)] @@ -106,4 +131,24 @@ mod tests { } Ok(()) } + + #[test] + fn test_clean_hub() -> anyhow::Result<()> { + yazi_shared::init_tests(); + + for (input, expected) in [ + ("test-hub://dotdot/@foo,root/foo/..", "test-hub://root/@/."), + ("test-hub://bar/@dotdot,foo,root/foo/../bar", "test-hub://bar/@root/bar"), + ("test-hub://dotdot/@root//..", "test-hub://root/@//"), + ("test-hub://bar/@dotdot,foo,root//foo/../bar", "test-hub://bar/@root//bar"), + ("test-hub://d3/@d2,d1,root/../../..", "test-hub://d3/@d2,d1,root/../../.."), + ] { + let input: UrlBuf = input.parse()?; + #[cfg(unix)] + assert_eq!(format!("{:?}", clean_url(input)), expected); + #[cfg(windows)] + assert_eq!(format!("{:?}", clean_url(input)).replace(r"\", "/"), expected); + } + Ok(()) + } } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 18d95794..1b8d9d71 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -40,6 +40,10 @@ fn expand_url_impl(url: UrlCow) -> UrlCow { loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), auth: auth.clone(), }, + Url::Hub { auth, .. } => UrlBuf::Hub { + auth: auth.clone().with_parent_depth(path.components().auth_depth()), + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + }, Url::Scope { auth, .. } => UrlBuf::Scope { loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), auth: auth.clone(), @@ -112,6 +116,7 @@ mod tests { ("test-mount://7z:2:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:3:2//tmp/test.zip/bar/baz"), ("test-mount://7z:2:2//tmp/$BAR_BAZ/test.zip", "test-mount://7z:3:3//tmp/bar/baz/test.zip"), ("test-mount://7z:2:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2:2//bar/baz/tmp/test.zip"), + ("test-hub://a1/@root/$BAR_BAZ", "test-hub://a1:2:2/@,root/bar/baz"), // -1 component ("test-mount://7z//tmp/test.zip/${BAR/BAZ}", "test-mount://7z//tmp/test.zip/bar_baz"), ("test-mount://7z:1//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/${BAR/BAZ}"), @@ -134,6 +139,7 @@ mod tests { ), ("test-mount://7z:3:3//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:2//tmp/test.zip/bar_baz"), ("test-mount://7z:3:3//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2:2//tmp/bar_baz/test.zip"), + ("test-hub://a1:2:2/@b1,root/${BAR/BAZ}", "test-hub://a1/@root/bar_baz"), // Zeros all components ("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"), ("test-mount://7z:1//${EM/PT/Y}", "test-mount://7z:1//${EM/PT/Y}"), diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index 0656a347..2b5358da 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -4,8 +4,16 @@ use anyhow::Result; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; use yazi_shared::path::{PathCow, PathDyn, PathKind}; -const SET: &AsciiSet = - &CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); +const SET: &AsciiSet = &CONTROLS + .add(b'"') + .add(b'*') + .add(b':') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'\\') + .add(b'|') + .add(b'%'); pub trait PercentEncoding<'a> { fn percent_encode(self) -> Cow<'a, Path>; diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 7ff7acc0..13751e30 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -67,8 +67,8 @@ impl FilesSorter { }), SortBy::Size => items.sort_unstable_by(|a, b| { promote!(a, b); - let aa = if a.is_dir() { sizes.get(&a.urn()).copied() } else { None }; - let bb = if b.is_dir() { sizes.get(&b.urn()).copied() } else { None }; + let aa = if a.is_dir() { sizes.get(&a.entry_key()).copied() } else { None }; + let bb = if b.is_dir() { sizes.get(&b.entry_key()).copied() } else { None }; self.fallback(a, b, self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len))) }), SortBy::Random => { diff --git a/yazi-fs/src/spec.rs b/yazi-fs/src/spec.rs index 1af5029a..6768e29a 100644 --- a/yazi-fs/src/spec.rs +++ b/yazi-fs/src/spec.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use yazi_shared::{auth::{Auth, AuthKind, Encode}, path::PathBufDyn, spec::SpecInventory}; +use yazi_shared::{auth::{Auth, AuthKind, EncodeAuth}, path::PathBufDyn, spec::SpecInventory}; use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; use crate::Xdg; @@ -13,12 +13,14 @@ impl FsSpec for Auth { fn cache(&self) -> Option { match self.kind { AuthKind::Regular | AuthKind::Search => None, - AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Some(Xdg::temp_dir().join(format!( - "{}_{}_{}", - self.kind.into_str(), - self.scheme, - Encode::domain(&self.domain) - ))), + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { + Some(Xdg::temp_dir().join(format!( + "{}_{}_{}", + self.kind.into_str(), + self.scheme, + EncodeAuth::domain(&self.domain) + ))) + } } } } diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index 32f14201..effbf782 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; use mlua::UserDataFields; -use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; +use yazi_shared::{path::{DynPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; use yazi_shim::mlua::UserDataFieldsExt; use crate::{FsHash128, FsSpec, path::PercentEncoding}; @@ -29,7 +29,7 @@ impl<'a> FsUrl<'a> for Url<'a> { fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); if it.next() == Some(yazi_shared::path::Component::RootDir) { - root.push(it.as_path().percent_encode()); + root.push(it.dyn_path().percent_encode()); } else { root.push(".%2F"); root.push(loc.percent_encode()); @@ -51,7 +51,7 @@ impl<'a> FsUrl<'a> for Url<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), - Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -66,7 +66,7 @@ impl FsUrl<'_> for UrlBuf { fn unified_path(self) -> Cow<'static, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), - Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -81,7 +81,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(), - Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } diff --git a/yazi-parser/src/mgr/hover.rs b/yazi-parser/src/mgr/hover.rs index cab56f6f..d0138841 100644 --- a/yazi-parser/src/mgr/hover.rs +++ b/yazi-parser/src/mgr/hover.rs @@ -3,11 +3,11 @@ use yazi_shared::path::PathBufDyn; #[derive(Debug, Default)] pub struct HoverForm { - pub urn: Option, + pub key: Option, } impl From> for HoverForm { - fn from(urn: Option) -> Self { Self { urn } } + fn from(key: Option) -> Self { Self { key } } } impl FromLua for HoverForm { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 17cd8e47..57b32469 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -206,7 +206,7 @@ impl Drop for FileInCut { impl FileInCut { pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self { Self { - follow: !from.auth().covariant(to.auth()), + follow: !from.auth().same_service(to.auth()), id: Id::ZERO, from, to, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 67e5ac9b..1e2fb594 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -78,7 +78,7 @@ impl Scheduler { } pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { - let follow = follow || !from.auth().covariant(to.auth()); + let follow = follow || !from.auth().same_service(to.auth()); let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 }; self.add(&mut r#in, |_| ()); @@ -109,7 +109,7 @@ impl Scheduler { let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow }; self.add(&mut r#in, |_| ()); - if !r#in.from.auth().covariant(r#in.to.auth()) { + if !r#in.from.auth().same_service(r#in.to.auth()) { return self .ops .out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned())); diff --git a/yazi-scheduler/src/size/size.rs b/yazi-scheduler/src/size/size.rs index bcd01646..0497ea80 100644 --- a/yazi-scheduler/src/size/size.rs +++ b/yazi-scheduler/src/size/size.rs @@ -35,9 +35,10 @@ impl Size { } let parent = buf[0].0.parent().unwrap(); + // FIXME: use PathBufDyn instead of UrlBuf in SizeIn FilesOp::Size( parent.into(), - HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().into(), s))), + HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.entry_key().into(), s))), ) .emit(); }); diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index d1027b76..16bbebe1 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -1,19 +1,37 @@ -use std::{fmt, sync::Arc}; +use std::{fmt, hash::{Hash, Hasher}, sync::Arc}; use anyhow::{Result, anyhow}; use percent_encoding::percent_decode_str; use serde::Deserialize; -use yazi_shim::{SStr, cell::RoCell}; +use yazi_shim::cell::RoCell; -use crate::auth::{AuthInventory, AuthKind, Encode, Scheme}; +use crate::{auth::{AuthInventory, AuthKind, Domain, EncodeAuth, Scheme}, path::{Component, Components}}; pub(super) static DEFAULT_ARC: RoCell> = RoCell::new(); -#[derive(Debug, Deserialize, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize)] pub struct Auth { pub kind: AuthKind, pub scheme: Scheme, - pub domain: SStr, + pub domain: Domain<'static>, + #[serde(default)] + pub parent: Option>, +} + +impl PartialEq for Auth { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind && self.scheme == other.scheme && self.domain == other.domain + } +} + +impl Eq for Auth {} + +impl Hash for Auth { + fn hash(&self, state: &mut H) { + self.kind.hash(state); + self.scheme.hash(state); + self.domain.hash(state); + } } impl Default for Auth { @@ -21,20 +39,28 @@ impl Default for Auth { } impl fmt::Display for Auth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Encode(self, false).fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { EncodeAuth(self, false).fmt(f) } } impl Auth { - pub const DEFAULT: Self = - Self { kind: AuthKind::Regular, scheme: Scheme::Regular, domain: SStr::Borrowed("") }; + pub const DEFAULT: Self = Self { + kind: AuthKind::Regular, + scheme: Scheme::Regular, + domain: Domain::EMPTY, + parent: None, + }; pub fn default_arc() -> Arc { DEFAULT_ARC.clone() } - pub fn new(kind: AuthKind, scheme: Scheme, domain: impl Into) -> Arc { - Arc::new(Self { kind, scheme, domain: domain.into() }) + pub fn new<'a>(kind: AuthKind, scheme: Scheme, domain: impl Into>) -> Arc { + Arc::new(Self { kind, scheme, domain: domain.into().into_owned(), parent: None }) } - pub fn get(scheme: &Scheme, domain: &str) -> Option> { + pub fn search<'a>(query: impl Into>) -> Arc { + Self::new(AuthKind::Search, Scheme::Search, query) + } + + pub fn get(scheme: &Scheme, domain: &Domain<'_>) -> Option> { match scheme { Scheme::Regular => Some(Self::default_arc()), Scheme::Search => Some(Self::search(domain)), @@ -42,22 +68,88 @@ impl Auth { } } - pub fn search(query: impl Into) -> Arc { - Self::new(AuthKind::Search, Scheme::Search, query.into()) + pub fn child(self: Arc) -> Arc { + Arc::new(Self { + kind: self.kind, + scheme: self.scheme.clone(), + domain: Domain::default(), + parent: Some(self), + }) + } + + pub fn root(mut self: &Arc) -> &Arc { + while let Some(parent) = &self.parent { + self = parent; + } + self + } + + pub fn descend<'a>(mut self: Arc, components: impl Into>) -> Arc { + for component in components.into() { + match component { + Component::RootDir => self = Self::new(self.kind, self.scheme.clone(), Domain::EMPTY), + c if c.has_auth() => self = self.child(), + _ => {} + } + } + self + } + + pub fn parent_at(mut self: &Arc, depth: usize) -> &Arc { + for _ in 0..depth { + self = self.parent.as_ref().expect("Auth parent depth out of bounds"); + } + self + } + + pub fn with_parent_depth(mut self: Arc, depth: usize) -> Arc { + let current = self.parent_depth(); + if current == depth { + return self; + } + + let mut parent = if current < depth { + self.parent.clone() + } else { + self.parent_at(current - depth).parent.clone() + }; + + for _ in current..depth { + parent = Some(Arc::new(Self { + kind: self.kind, + scheme: self.scheme.clone(), + domain: Domain::default(), + parent, + })); + } + + Arc::make_mut(&mut self).parent = parent; + self + } + + pub fn parent_depth(&self) -> usize { + let mut depth = 0; + let mut parent = self.parent.as_deref(); + while let Some(auth) = parent { + depth += 1; + parent = auth.parent.as_deref(); + } + depth } pub fn covariant(&self, other: &Self) -> bool { !self.kind.is_virtual() && !other.kind.is_virtual() || self == other } + pub fn same_service(&self, other: &Self) -> bool { + self.covariant(other) + || self.kind == AuthKind::Hub && other.kind == AuthKind::Hub && self.scheme == other.scheme + } + pub fn parse_cache(cache: &str) -> Result> { let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; - Ok(Arc::new(Self { - kind: kind.parse()?, - scheme: scheme.parse()?, - domain: percent_decode_str(domain).decode_utf8()?.into_owned().into(), - })) + Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?)) } } diff --git a/yazi-shared/src/auth/domain.rs b/yazi-shared/src/auth/domain.rs new file mode 100644 index 00000000..98297e98 --- /dev/null +++ b/yazi-shared/src/auth/domain.rs @@ -0,0 +1,69 @@ +use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref}; + +use serde::{Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, value::CowStrDeserializer}}; + +#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(transparent)] +pub struct Domain<'a>(Cow<'a, str>); + +impl Domain<'static> { + pub const CATCHALL: Self = Self(Cow::Borrowed("*")); + pub const EMPTY: Self = Self(Cow::Borrowed("")); +} + +impl Domain<'_> { + #[inline] + pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) } + + #[inline] + pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL } +} + +impl Deref for Domain<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Borrow for Domain<'_> { + fn borrow(&self) -> &str { &self.0 } +} + +impl AsRef for Domain<'_> { + fn as_ref(&self) -> &str { &self.0 } +} + +impl<'a> From for Domain<'a> { + fn from(value: String) -> Self { Self(Cow::Owned(value)) } +} + +impl<'a> From<&'a str> for Domain<'a> { + fn from(value: &'a str) -> Self { Self(Cow::Borrowed(value)) } +} + +impl<'a> From> for Domain<'a> { + fn from(value: Cow<'a, str>) -> Self { Self(value) } +} + +impl<'a> From<&'a Domain<'_>> for Domain<'a> { + fn from(value: &'a Domain<'_>) -> Self { Self(Cow::Borrowed(&value.0)) } +} + +impl Display for Domain<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) } +} + +impl<'de> Deserialize<'de> for Domain<'_> { + fn deserialize>(deserializer: D) -> Result { + Ok(Self(Cow::Owned(String::deserialize(deserializer)?))) + } +} + +impl<'de, 'a, E> IntoDeserializer<'de, E> for Domain<'a> +where + E: de::Error, +{ + type Deserializer = CowStrDeserializer<'a, E>; + + fn into_deserializer(self) -> Self::Deserializer { self.0.into_deserializer() } +} diff --git a/yazi-shared/src/auth/encode.rs b/yazi-shared/src/auth/encode.rs index 043da080..662a8b0e 100644 --- a/yazi-shared/src/auth/encode.rs +++ b/yazi-shared/src/auth/encode.rs @@ -2,18 +2,18 @@ use std::fmt; use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; -use super::Auth; +use super::{Auth, AuthKind, Domain}; -pub struct Encode<'a>(pub &'a Auth, pub bool); +pub struct EncodeAuth<'a>(pub &'a Auth, pub bool); -impl Encode<'_> { - pub fn domain(s: &str) -> PercentEncode<'_> { - const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); +impl EncodeAuth<'_> { + pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%'); percent_encode(s.as_bytes(), SET) } } -impl fmt::Display for Encode<'_> { +impl fmt::Display for EncodeAuth<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -24,3 +24,32 @@ impl fmt::Display for Encode<'_> { ) } } + +// --- EncodePrefix +pub struct EncodePrefix<'a>(pub &'a Auth); + +impl EncodePrefix<'_> { + pub fn parent<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b',').add(b'@').add(b'%'); + percent_encode(s.as_bytes(), SET) + } +} + +impl fmt::Display for EncodePrefix<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.kind != AuthKind::Hub { + return f.write_str("/"); + } + + f.write_str("/@")?; + let (mut first, mut parent) = (true, self.0.parent.as_deref()); + while let Some(auth) = parent { + if !first { + f.write_str(",")?; + } + Self::parent(&auth.domain).fmt(f)?; + (first, parent) = (false, auth.parent.as_deref()); + } + f.write_str("/") + } +} diff --git a/yazi-shared/src/auth/inventory.rs b/yazi-shared/src/auth/inventory.rs index a722d68d..b16dcb53 100644 --- a/yazi-shared/src/auth/inventory.rs +++ b/yazi-shared/src/auth/inventory.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use crate::auth::{Auth, Scheme}; +use crate::auth::{Auth, Domain, Scheme}; pub struct AuthInventory { - pub get: fn(&Scheme, &str) -> Option>, + pub get: fn(&Scheme, &Domain<'_>) -> Option>, } inventory::collect!(AuthInventory); diff --git a/yazi-shared/src/auth/kind.rs b/yazi-shared/src/auth/kind.rs index ed794c77..35f00d9b 100644 --- a/yazi-shared/src/auth/kind.rs +++ b/yazi-shared/src/auth/kind.rs @@ -11,6 +11,7 @@ pub enum AuthKind { Regular, Search, Mount, + Hub, Scope, Sftp, } @@ -20,7 +21,7 @@ impl AuthKind { pub fn is_local(self) -> bool { match self { Self::Regular | Self::Search => true, - Self::Mount | Self::Scope | Self::Sftp => false, + Self::Mount | Self::Hub | Self::Scope | Self::Sftp => false, } } @@ -28,7 +29,7 @@ impl AuthKind { pub fn is_remote(self) -> bool { match self { Self::Regular | Self::Search | Self::Mount => false, - Self::Scope | Self::Sftp => true, + Self::Hub | Self::Scope | Self::Sftp => true, } } @@ -36,7 +37,7 @@ impl AuthKind { pub fn is_virtual(self) -> bool { match self { Self::Regular | Self::Search => false, - Self::Mount | Self::Scope | Self::Sftp => true, + Self::Mount | Self::Hub | Self::Scope | Self::Sftp => true, } } } diff --git a/yazi-shared/src/auth/mod.rs b/yazi-shared/src/auth/mod.rs index 55f581e7..8ce33bb5 100644 --- a/yazi-shared/src/auth/mod.rs +++ b/yazi-shared/src/auth/mod.rs @@ -1,3 +1,3 @@ -yazi_macro::mod_flat!(auth encode inventory kind scheme); +yazi_macro::mod_flat!(auth domain encode inventory kind scheme); pub(super) fn init() { DEFAULT_ARC.with(Default::default); } diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs index a87e17a1..4be601a5 100644 --- a/yazi-shared/src/loc/able.rs +++ b/yazi-shared/src/loc/able.rs @@ -1,6 +1,6 @@ use std::{ffi::{OsStr, OsString}, fmt::Debug, hash::Hash}; -use crate::{path::{AsPath, AsPathView}, strand::AsStrandView}; +use crate::{path::{DynPath, PathView}, strand::AsStrandView}; // --- LocAble pub trait LocAble<'p> @@ -27,14 +27,10 @@ impl<'p> LocAble<'p> for &'p typed_path::UnixPath { // --- LocBufAble pub trait LocBufAble where - Self: 'static + AsPath + Default, + Self: 'static + DynPath + Default, { type Strand<'a>: StrandAble<'a>; - type Borrowed<'a>: LocAble<'a> - + LocAbleImpl<'a> - + AsPathView<'a, Self::Borrowed<'a>> - + Debug - + Hash; + type Borrowed<'a>: LocAble<'a> + LocAbleImpl<'a> + PathView<'a, Self::Borrowed<'a>> + Debug + Hash; } impl LocBufAble for std::path::PathBuf { diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index f00c77eb..eef2cb78 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -2,7 +2,7 @@ use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, use anyhow::Result; -use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, strand::AsStrandView}; +use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{DynPath, PathDyn, PathView, SetNameError}, strand::AsStrandView}; #[derive(Clone, Default, Eq, PartialEq)] pub struct LocBuf

{ @@ -25,18 +25,18 @@ impl AsRef for LocBuf { fn as_ref(&self) -> &std::path::Path { self.inner.as_ref() } } -impl AsPath for LocBuf +impl DynPath for LocBuf where - T: LocBufAble + AsPath, + T: LocBufAble + DynPath, { - fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } + fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() } } -impl AsPath for &LocBuf +impl DynPath for &LocBuf where - T: LocBufAble + AsPath, + T: LocBufAble + DynPath, { - fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } + fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() } } impl

Ord for LocBuf

@@ -59,7 +59,7 @@ where impl

Hash for LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { fn hash(&self, state: &mut H) { self.as_loc().hash(state) } } @@ -67,7 +67,7 @@ where impl

Debug for LocBuf

where P: LocBufAble + LocBufAbleImpl + Debug, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("LocBuf") @@ -81,7 +81,7 @@ where impl

From

for LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { fn from(path: P) -> Self { let Loc { inner, uri, urn, _phantom } = Loc::bare(&path); @@ -100,7 +100,7 @@ impl> From<&T> for LocBuf { impl

LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { pub fn new<'a, S>(path: P, base: S, trail: S) -> Self where @@ -157,7 +157,7 @@ where #[inline] pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> { Loc { - inner: self.inner.as_path_view(), + inner: self.inner.path_view(), uri: self.uri, urn: self.urn, _phantom: PhantomData, @@ -223,7 +223,7 @@ where impl

LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { #[inline] pub fn uri(&self) -> P::Borrowed<'_> { self.as_loc().uri() } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 533b7c2a..df983bab 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -3,7 +3,7 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref}; use anyhow::{Result, bail}; use super::LocAbleImpl; -use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, strand::AsStrandView}; +use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{DynPath, PathDyn, PathView}, strand::AsStrandView}; #[derive(Clone, Copy, Debug)] pub struct Loc<'p, P = &'p std::path::Path> { @@ -29,11 +29,11 @@ where fn deref(&self) -> &Self::Target { &self.inner } } -impl<'p, P> AsPath for Loc<'p, P> +impl<'p, P> DynPath for Loc<'p, P> where - P: LocAble<'p> + AsPath, + P: LocAble<'p> + DynPath, { - fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } + fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() } } // FIXME: remove @@ -81,9 +81,9 @@ where pub fn bare(path: T) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { - let path = path.as_path_view(); + let path = path.path_view(); let Some(name) = path.file_name() else { let p = path.strip_prefix(P::empty()).unwrap(); return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData }; @@ -114,7 +114,7 @@ where pub fn floated<'a, T, S>(path: T, base: S) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); @@ -133,7 +133,7 @@ where pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); @@ -143,18 +143,17 @@ where } #[inline] - pub fn parent(self) -> Option

{ - self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty()) - } + pub fn parent(self) -> Option

{ self.inner.parent() } pub fn saturated<'a, T>(path: T, kind: AuthKind) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { match kind { AuthKind::Regular => Self::bare(path), AuthKind::Search => Self::zeroed(path), AuthKind::Mount => Self::zeroed(path), + AuthKind::Hub => Self::bare(path), AuthKind::Scope => Self::bare(path), AuthKind::Sftp => Self::bare(path), } @@ -206,7 +205,7 @@ where pub fn with(path: T, uri: usize, urn: usize) -> Result where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { if urn > uri { bail!("URN cannot be longer than URI"); @@ -238,7 +237,7 @@ where pub fn zeroed(path: T) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { let mut loc = Self::bare(path); (loc.uri, loc.urn) = (0, 0); @@ -248,8 +247,21 @@ where #[cfg(test)] mod tests { + use std::path::Path; + + use typed_path::UnixPath; + use super::*; + #[test] + fn test_parent() { + assert_eq!(Loc::bare(Path::new("foo")).parent(), Some(Path::new(""))); + assert_eq!(Loc::bare(Path::new("")).parent(), None); + + assert_eq!(Loc::bare(UnixPath::new("foo")).parent(), Some(UnixPath::new(""))); + assert_eq!(Loc::bare(UnixPath::new("")).parent(), None); + } + #[test] fn test_with() -> Result<()> { let cases = [ @@ -268,11 +280,11 @@ mod tests { ]; for (path, uri, urn, expect_uri, expect_urn) in cases { - let loc = Loc::with(std::path::Path::new(path), uri, urn)?; + let loc = Loc::with(Path::new(path), uri, urn)?; assert_eq!(loc.uri().to_str().unwrap(), expect_uri); assert_eq!(loc.urn().to_str().unwrap(), expect_urn); - let loc = Loc::with(typed_path::UnixPath::new(path), uri, urn)?; + let loc = Loc::with(UnixPath::new(path), uri, urn)?; assert_eq!(loc.uri().to_str().unwrap(), expect_uri); assert_eq!(loc.urn().to_str().unwrap(), expect_urn); } diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index b0e0f20a..70815f9f 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use yazi_codegen::FromLuaOwned; use yazi_shim::wtf8::FromWtf8Vec; -use crate::{path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand}; +use crate::{path::{Component, DynPath, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand}; // --- PathBufDyn #[derive(Clone, Debug, Eq, FromLuaOwned)] @@ -48,24 +48,24 @@ impl TryFrom for typed_path::UnixPathBuf { // --- Hash impl Hash for PathBufDyn { - fn hash(&self, state: &mut H) { self.as_path().hash(state) } + fn hash(&self, state: &mut H) { self.dyn_path().hash(state) } } // --- PartialEq impl PartialEq for PathBufDyn { - fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } + fn eq(&self, other: &Self) -> bool { self.dyn_path() == other.dyn_path() } } impl PartialEq> for PathBufDyn { - fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } + fn eq(&self, other: &PathDyn<'_>) -> bool { self.dyn_path() == *other } } impl PartialEq> for &PathBufDyn { - fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } + fn eq(&self, other: &PathDyn<'_>) -> bool { self.dyn_path() == *other } } impl Equivalent> for PathBufDyn { - fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.as_path() == *key } + fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.dyn_path() == *key } } impl PathBufDyn { @@ -126,7 +126,7 @@ impl PathBufDyn { pub fn try_extend(&mut self, paths: T) -> Result<(), PathDynError> where T: IntoIterator, - T::Item: AsPath, + T::Item: DynPath, { for p in paths { self.try_push(p)?; @@ -136,9 +136,9 @@ impl PathBufDyn { pub fn try_push(&mut self, path: T) -> Result<(), PathDynError> where - T: AsPath, + T: DynPath, { - let path = path.as_path(); + let path = path.dyn_path(); Ok(match self { Self::Os(p) => p.push(path.as_os()?), Self::Unix(p) => p.push(path.encoded_bytes()), @@ -199,7 +199,7 @@ impl Serialize for PathBufDyn { path: &'a [u8], } - let path = self.as_path(); + let path = self.dyn_path(); Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer) } } diff --git a/yazi-shared/src/path/component.rs b/yazi-shared/src/path/component.rs index ec60377b..d8dad5bc 100644 --- a/yazi-shared/src/path/component.rs +++ b/yazi-shared/src/path/component.rs @@ -18,6 +18,9 @@ impl<'a> Component<'a> { _ => None, } } + + #[inline] + pub fn has_auth(&self) -> bool { matches!(self, Self::ParentDir | Self::Normal(_)) } } impl<'a> From> for Component<'a> { diff --git a/yazi-shared/src/path/components.rs b/yazi-shared/src/path/components.rs index 37273b3b..3a4b7a56 100644 --- a/yazi-shared/src/path/components.rs +++ b/yazi-shared/src/path/components.rs @@ -10,7 +10,17 @@ pub enum Components<'a> { Unix(typed_path::UnixComponents<'a>), } +impl<'a> From<&'a std::path::Path> for Components<'a> { + fn from(value: &'a std::path::Path) -> Self { Self::Os(value.components()) } +} + +impl<'a> From<&'a typed_path::UnixPath> for Components<'a> { + fn from(value: &'a typed_path::UnixPath) -> Self { Self::Unix(value.components()) } +} + impl<'a> Components<'a> { + pub fn auth_depth(self) -> usize { self.filter(Component::has_auth).count() } + pub fn path(&self) -> PathDyn<'a> { match self { Self::Os(c) => PathDyn::Os(c.as_path()), diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs deleted file mode 100644 index 5478b1ff..00000000 --- a/yazi-shared/src/path/conversion.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::{PathBufDyn, PathDyn}; -use crate::path::PathCow; - -// --- AsPath -pub trait AsPath { - fn as_path(&self) -> PathDyn<'_>; -} - -impl AsPath for std::path::Path { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } -} - -impl AsPath for std::path::PathBuf { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } -} - -impl AsPath for typed_path::UnixPath { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } -} - -impl AsPath for typed_path::UnixPathBuf { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } -} - -impl AsPath for PathDyn<'_> { - fn as_path(&self) -> PathDyn<'_> { *self } -} - -impl AsPath for PathBufDyn { - fn as_path(&self) -> PathDyn<'_> { - match self { - Self::Os(p) => PathDyn::Os(p), - Self::Unix(p) => PathDyn::Unix(p), - } - } -} - -impl AsPath for PathCow<'_> { - fn as_path(&self) -> PathDyn<'_> { - match self { - PathCow::Borrowed(p) => *p, - PathCow::Owned(p) => p.as_path(), - } - } -} - -impl AsPath for super::Components<'_> { - fn as_path(&self) -> PathDyn<'_> { self.path() } -} - -// --- AsPathRef -pub trait AsPathRef<'a> { - fn as_path_ref(self) -> PathDyn<'a>; -} - -impl<'a> AsPathRef<'a> for PathDyn<'a> { - fn as_path_ref(self) -> Self { self } -} diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index 96ac3053..48033d4f 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use anyhow::Result; -use crate::path::{AsPath, PathBufDyn, PathDyn, PathDynError, PathKind}; +use crate::path::{DynPath, PathBufDyn, PathDyn, PathDynError, PathKind}; #[derive(Debug)] pub enum PathCow<'a> { @@ -23,7 +23,7 @@ impl<'a> From for PathCow<'a> { } impl<'a> From<&'a PathCow<'_>> for PathCow<'a> { - fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.as_path()) } + fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.dyn_path()) } } impl From> for PathBufDyn { @@ -31,14 +31,14 @@ impl From> for PathBufDyn { } impl PartialEq for PathCow<'_> { - fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } + fn eq(&self, other: &Self) -> bool { self.dyn_path() == other.dyn_path() } } impl PartialEq<&str> for PathCow<'_> { fn eq(&self, other: &&str) -> bool { match self { - Self::Borrowed(s) => s.as_path() == *other, - Self::Owned(s) => s.as_path() == *other, + Self::Borrowed(s) => s.dyn_path() == *other, + Self::Owned(s) => s.dyn_path() == *other, } } } diff --git a/yazi-shared/src/path/dyn_path.rs b/yazi-shared/src/path/dyn_path.rs new file mode 100644 index 00000000..fbe1e783 --- /dev/null +++ b/yazi-shared/src/path/dyn_path.rs @@ -0,0 +1,66 @@ +use super::{PathBufDyn, PathDyn}; +use crate::path::PathCow; + +// --- DynPath +pub trait DynPath { + fn dyn_path(&self) -> PathDyn<'_>; +} + +impl DynPath for std::path::Path { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } +} + +impl DynPath for std::path::PathBuf { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } +} + +impl DynPath for typed_path::UnixPath { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } +} + +impl DynPath for typed_path::UnixPathBuf { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } +} + +impl DynPath for PathDyn<'_> { + fn dyn_path(&self) -> PathDyn<'_> { *self } +} + +impl DynPath for PathBufDyn { + fn dyn_path(&self) -> PathDyn<'_> { + match self { + Self::Os(p) => PathDyn::Os(p), + Self::Unix(p) => PathDyn::Unix(p), + } + } +} + +impl DynPath for PathCow<'_> { + fn dyn_path(&self) -> PathDyn<'_> { + match self { + PathCow::Borrowed(p) => *p, + PathCow::Owned(p) => p.dyn_path(), + } + } +} + +impl DynPath for super::Components<'_> { + fn dyn_path(&self) -> PathDyn<'_> { self.path() } +} + +// --- DynPathRef +pub trait DynPathRef<'a> { + fn dyn_path_ref(self) -> PathDyn<'a>; +} + +impl<'a> DynPathRef<'a> for PathDyn<'a> { + fn dyn_path_ref(self) -> Self { self } +} + +impl<'a> DynPathRef<'a> for &'a std::path::Path { + fn dyn_path_ref(self) -> PathDyn<'a> { PathDyn::Os(self) } +} + +impl<'a> DynPathRef<'a> for &'a typed_path::UnixPath { + fn dyn_path_ref(self) -> PathDyn<'a> { PathDyn::Unix(self) } +} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index 68b59865..7e64828f 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -15,6 +15,7 @@ impl From for PathKind { AuthKind::Regular => Self::Os, AuthKind::Search => Self::Os, AuthKind::Mount => Self::Os, + AuthKind::Hub => Self::Os, AuthKind::Scope => Self::Unix, AuthKind::Sftp => Self::Unix, } diff --git a/yazi-shared/src/path/like.rs b/yazi-shared/src/path/like.rs index 2c5ef1f2..0036e38e 100644 --- a/yazi-shared/src/path/like.rs +++ b/yazi-shared/src/path/like.rs @@ -2,95 +2,97 @@ use std::borrow::Cow; use anyhow::Result; -use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}}; +use crate::{Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}}; -pub trait PathLike: AsPath { - fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() } +pub trait PathLike: DynPath { + fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.dyn_path().as_os() } - fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.as_path().as_unix() } + fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.dyn_path().as_unix() } - fn components(&self) -> Components<'_> { self.as_path().components() } + fn components(&self) -> Components<'_> { self.dyn_path().components() } - fn display(&self) -> Display<'_> { self.as_path().display() } + fn display(&self) -> Display<'_> { self.dyn_path().display() } - fn encoded_bytes(&self) -> &[u8] { self.as_path().encoded_bytes() } + fn encoded_bytes(&self) -> &[u8] { self.dyn_path().encoded_bytes() } - fn ext(&self) -> Option> { self.as_path().ext() } + fn ext(&self) -> Option> { self.dyn_path().ext() } - fn has_root(&self) -> bool { self.as_path().has_root() } + fn has_root(&self) -> bool { self.dyn_path().has_root() } - fn is_absolute(&self) -> bool { self.as_path().is_absolute() } + fn is_absolute(&self) -> bool { self.dyn_path().is_absolute() } - fn is_empty(&self) -> bool { self.as_path().is_empty() } + fn is_empty(&self) -> bool { self.dyn_path().is_empty() } #[cfg(unix)] - fn is_hidden(&self) -> bool { self.as_path().is_hidden() } + fn is_hidden(&self) -> bool { self.dyn_path().is_hidden() } - fn kind(&self) -> PathKind { self.as_path().kind() } + fn kind(&self) -> PathKind { self.dyn_path().kind() } - fn len(&self) -> usize { self.as_path().len() } + fn len(&self) -> usize { self.dyn_path().len() } - fn name(&self) -> Option> { self.as_path().name() } + fn name(&self) -> Option> { self.dyn_path().name() } - fn parent(&self) -> Option> { self.as_path().parent() } + fn parent(&self) -> Option> { self.dyn_path().parent() } fn rsplit_pred(&self, pred: T) -> Option<(PathDyn<'_>, PathDyn<'_>)> where T: Utf8BytePredictor, { - self.as_path().rsplit_pred(pred) + self.dyn_path().rsplit_pred(pred) } - fn stem(&self) -> Option> { self.as_path().stem() } + fn stem(&self) -> Option> { self.dyn_path().stem() } - fn to_os_owned(&self) -> Result { self.as_path().to_os_owned() } + fn to_os_owned(&self) -> Result { + self.dyn_path().to_os_owned() + } - fn to_owned(&self) -> PathBufDyn { self.as_path().to_owned() } + fn to_owned(&self) -> PathBufDyn { self.dyn_path().to_owned() } - fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.as_path().to_str() } + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.dyn_path().to_str() } - fn to_string_lossy(&self) -> Cow<'_, str> { self.as_path().to_string_lossy() } + fn to_string_lossy(&self) -> Cow<'_, str> { self.dyn_path().to_string_lossy() } fn try_ends_with(&self, child: T) -> Result where T: AsStrand, { - self.as_path().try_ends_with(child) + self.dyn_path().try_ends_with(child) } fn try_join(&self, path: T) -> Result where T: AsStrand, { - self.as_path().try_join(path) + self.dyn_path().try_join(path) } fn try_rsplit_seq(&self, pat: T) -> Result<(PathDyn<'_>, PathDyn<'_>), RsplitOnceError> where T: AsStrand, { - self.as_path().try_rsplit_seq(pat) + self.dyn_path().try_rsplit_seq(pat) } fn try_starts_with(&self, base: T) -> Result where T: AsStrand, { - self.as_path().try_starts_with(base) + self.dyn_path().try_starts_with(base) } fn try_strip_prefix(&self, base: T) -> Result, StripPrefixError> where T: AsStrand, { - self.as_path().try_strip_prefix(base) + self.dyn_path().try_strip_prefix(base) } fn try_strip_suffix(&self, suffix: T) -> Result, StripSuffixError> where T: AsStrand, { - self.as_path().try_strip_suffix(suffix) + self.dyn_path().try_strip_suffix(suffix) } } diff --git a/yazi-shared/src/path/mod.rs b/yazi-shared/src/path/mod.rs index fe574952..a7d430fb 100644 --- a/yazi-shared/src/path/mod.rs +++ b/yazi-shared/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component components conversion cow display error kind like lua path view); +yazi_macro::mod_flat!(buf component components cow display dyn_path error kind like lua path view); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index d01c63a5..f8cb1b83 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -4,7 +4,7 @@ use anyhow::Result; use hashbrown::Equivalent; use super::{RsplitOnceError, StartsWithError}; -use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; +use crate::{BytesExt, Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum PathDyn<'p> { @@ -21,11 +21,11 @@ impl<'a> From<&'a typed_path::UnixPath> for PathDyn<'a> { } impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { - fn from(value: &'a PathBufDyn) -> Self { value.as_path() } + fn from(value: &'a PathBufDyn) -> Self { value.dyn_path() } } impl PartialEq for PathDyn<'_> { - fn eq(&self, other: &PathBufDyn) -> bool { *self == other.as_path() } + fn eq(&self, other: &PathBufDyn) -> bool { *self == other.dyn_path() } } impl PartialEq> for &std::path::Path { @@ -36,6 +36,12 @@ impl PartialEq<&std::path::Path> for PathDyn<'_> { fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } } +impl PartialEq<&typed_path::UnixPath> for PathDyn<'_> { + fn eq(&self, other: &&typed_path::UnixPath) -> bool { + matches!(*self, PathDyn::Unix(p) if p == *other) + } +} + impl PartialEq<&str> for PathDyn<'_> { fn eq(&self, other: &&str) -> bool { match *self { @@ -46,7 +52,7 @@ impl PartialEq<&str> for PathDyn<'_> { } impl Equivalent for PathDyn<'_> { - fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.as_path() } + fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.dyn_path() } } impl<'p> PathDyn<'p> { @@ -139,8 +145,8 @@ impl<'p> PathDyn<'p> { pub fn parent(self) -> Option { Some(match self { - Self::Os(p) => Self::Os(p.parent().filter(|p| !p.as_os_str().is_empty())?), - Self::Unix(p) => Self::Unix(p.parent().filter(|p| !p.as_bytes().is_empty())?), + Self::Os(p) => Self::Os(p.parent()?), + Self::Unix(p) => Self::Unix(p.parent()?), }) } @@ -296,3 +302,34 @@ impl<'p> PathDyn<'p> { } } } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use typed_path::UnixPath; + + use super::*; + + #[test] + fn test_parent() { + let path = Path::new("foo").dyn_path(); + assert_eq!(path.parent(), Some(Path::new("").dyn_path())); + assert_eq!(path.parent().unwrap().parent(), None); + + let path = UnixPath::new("foo").dyn_path(); + assert_eq!(path.parent(), Some(UnixPath::new("").dyn_path())); + assert_eq!(path.parent().unwrap().parent(), None); + } + + #[cfg(windows)] + #[test] + fn test_windows_parent() { + let path = Path::new("foo").dyn_path(); + assert_eq!(path.parent(), Some(Path::new("").dyn_path())); + + let path = Path::new("C:foo").dyn_path(); + assert_eq!(path.parent(), Some(Path::new("C:").dyn_path())); + assert_eq!(path.parent().unwrap().parent(), None); + } +} diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs index 31a2fdf0..a2478259 100644 --- a/yazi-shared/src/path/view.rs +++ b/yazi-shared/src/path/view.rs @@ -1,20 +1,20 @@ -// --- AsPathView -pub trait AsPathView<'a, T> { - fn as_path_view(self) -> T; +// --- PathView +pub trait PathView<'a, T> { + fn path_view(self) -> T; } -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { - fn as_path_view(self) -> &'a std::path::Path { self } +impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::Path { + fn path_view(self) -> &'a std::path::Path { self } } -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { - fn as_path_view(self) -> &'a std::path::Path { self } +impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { + fn path_view(self) -> &'a std::path::Path { self } } -impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath { - fn as_path_view(self) -> &'a typed_path::UnixPath { self } +impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath { + fn path_view(self) -> &'a typed_path::UnixPath { self } } -impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf { - fn as_path_view(self) -> &'a typed_path::UnixPath { self } +impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf { + fn path_view(self) -> &'a typed_path::UnixPath { self } } diff --git a/yazi-shared/src/spec/encode.rs b/yazi-shared/src/spec/encode.rs index 79728da1..798f70a9 100644 --- a/yazi-shared/src/spec/encode.rs +++ b/yazi-shared/src/spec/encode.rs @@ -1,17 +1,17 @@ use std::fmt::{self, Display}; -use crate::{auth::{AuthKind, Encode as EncodeAuth}, url::Url}; +use crate::{auth::{AuthKind, EncodeAuth, EncodePrefix}, url::Url}; #[derive(Clone, Copy)] -pub struct Encode<'a>(pub Url<'a>); +pub struct EncodeSpec<'a>(pub Url<'a>); -impl<'a> From> for Encode<'a> { +impl<'a> From> for EncodeSpec<'a> { fn from(value: crate::url::Encode<'a>) -> Self { Self(value.0) } } -impl<'a> Encode<'a> { +impl<'a> EncodeSpec<'a> { pub(crate) fn ports(self) -> impl Display { - struct D<'a>(Encode<'a>); + struct D<'a>(EncodeSpec<'a>); impl Display for D<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -30,7 +30,7 @@ impl<'a> Encode<'a> { match self.0.0.kind() { AuthKind::Regular => Ok(()), AuthKind::Search | AuthKind::Mount => w!(0, 0), - AuthKind::Scope | AuthKind::Sftp => { + AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize) } } @@ -41,15 +41,16 @@ impl<'a> Encode<'a> { } } -impl Display for Encode<'_> { +impl Display for EncodeSpec<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { Url::Regular(_) => write!(f, "regular://"), Url::Search { auth, .. } | Url::Mount { auth, .. } + | Url::Hub { auth, .. } | Url::Scope { auth, .. } | Url::Sftp { auth, .. } => { - write!(f, "{}{}/", EncodeAuth(auth, false), self.ports()) + write!(f, "{}{}{}", EncodeAuth(auth, false), self.ports(), EncodePrefix(auth)) } } } diff --git a/yazi-shared/src/spec/spec.rs b/yazi-shared/src/spec/spec.rs index b33ad015..24b0b5f2 100644 --- a/yazi-shared/src/spec/spec.rs +++ b/yazi-shared/src/spec/spec.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, ensure}; use percent_encoding::percent_decode; use serde::Deserialize; -use crate::{auth::{Auth, AuthKind, Encode, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url}; +use crate::{auth::{Auth, AuthKind, Domain, EncodeAuth, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url}; #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub struct Spec { @@ -28,18 +28,23 @@ impl Spec { // Decode domain and ports let mut skip = 0; let (domain, uri, urn) = match &parsed.scheme { - Scheme::Regular => (Cow::Borrowed(""), None, None), + Scheme::Regular => (Domain::default(), None, None), _ => Self::decode_param(rest, &mut skip)?, }; // Resolve authority - let auth = Auth::get(&parsed.scheme, &domain) - .ok_or_else(|| anyhow!("unknown VFS authority: {parsed}://{}", Encode::domain(&domain)))?; + let auth = Auth::get(&parsed.scheme, &domain).ok_or_else(|| { + anyhow!("unknown VFS authority: {parsed}://{}", EncodeAuth::domain(&domain)) + })?; // Decode path - let path = Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?; - let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?; + let (path, auth) = if auth.kind == AuthKind::Hub { + Self::decode_hub(auth, parsed.tilde, &rest[skip..])? + } else { + (Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?, auth) + }; + let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?; Ok((Self { auth, uri, urn }, path)) } @@ -54,16 +59,13 @@ impl Spec { fn decode_param<'a>( bytes: &'a [u8], skip: &mut usize, - ) -> Result<(Cow<'a, str>, Option, Option)> { + ) -> Result<(Domain<'a>, Option, Option)> { let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); let slash = bytes.get(len).is_some_and(|&b| b == b'/'); *skip += len + slash as usize; let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; - let domain = match Cow::from(percent_decode(&bytes[..len])) { - Cow::Borrowed(b) => str::from_utf8(b)?.into(), - Cow::Owned(b) => String::from_utf8(b)?.into(), - }; + let domain = percent_decode(&bytes[..len]).decode_utf8()?.into(); Ok((domain, uri, urn)) } @@ -90,6 +92,41 @@ impl Spec { PathCow::with(kind, bytes) } + fn decode_hub<'a>( + mut auth: Arc, + tilde: bool, + bytes: &'a [u8], + ) -> Result<(PathCow<'a>, Arc)> { + ensure!(bytes.first() == Some(&b'@'), "Hub URL requires an `@` parent marker"); + let end = bytes[1..] + .iter() + .position(|&b| b == b'/') + .map(|i| i + 1) + .ok_or_else(|| anyhow!("Hub URL requires a path delimiter"))?; + + let path = Self::decode_path(AuthKind::Hub, tilde, &bytes[end + 1..])?; + let depth = path.components().auth_depth(); + if depth == 0 { + ensure!(bytes[1..end].is_empty(), "Hub URL has too many parent domains"); + return Ok((path, auth)); + } + + let (mut count, mut parent) = (0, None); + for domain in bytes[1..end].rsplit(|&b| b == b',') { + count += 1; + parent = Some(Arc::new(Auth { + kind: auth.kind, + scheme: auth.scheme.clone(), + domain: Domain::from(percent_decode(domain).decode_utf8()?).into_owned(), + parent, + })); + } + + ensure!(count == depth, "Hub URL parent depth does not match its path"); + Arc::make_mut(&mut auth).parent = parent; + Ok((path, auth)) + } + fn normalize_ports( kind: AuthKind, uri: Option, @@ -107,7 +144,7 @@ impl Spec { (uri, urn) } AuthKind::Mount => (uri.unwrap_or(0), urn.unwrap_or(0)), - AuthKind::Scope | AuthKind::Sftp => { + AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { let uri = uri.unwrap_or(path.name().is_some() as usize); let urn = urn.unwrap_or(path.name().is_some() as usize); (uri, urn) @@ -118,7 +155,7 @@ impl Spec { pub fn retrieve_ports(url: Url) -> (usize, usize) { match url { Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), - Url::Search { loc, .. } | Url::Mount { loc, .. } => { + Url::Search { loc, .. } | Url::Mount { loc, .. } | Url::Hub { loc, .. } => { (loc.uri().components().count(), loc.urn().components().count()) } Url::Scope { loc, .. } | Url::Sftp { loc, .. } => { diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index 7ed18e20..637ea9a2 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -22,6 +22,7 @@ impl From for StrandKind { AuthKind::Regular => Self::Os, AuthKind::Search => Self::Os, AuthKind::Mount => Self::Os, + AuthKind::Hub => Self::Os, AuthKind::Scope => Self::Bytes, AuthKind::Sftp => Self::Bytes, } diff --git a/yazi-shared/src/tests.rs b/yazi-shared/src/tests.rs index 0ac25d82..e3360402 100644 --- a/yazi-shared/src/tests.rs +++ b/yazi-shared/src/tests.rs @@ -10,8 +10,9 @@ pub fn init_tests() { inventory::submit! { AuthInventory { - get: |scheme, domain| match (scheme.as_str(), domain) { + get: |scheme, domain| match (scheme.as_str(), domain.as_ref()) { ("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), + ("test-hub", _) => Some(Auth::new(AuthKind::Hub, scheme.clone(), domain.clone())), ("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), ("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), _ => None, diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index b86f4f4f..14ba6c83 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize, de::{self, IntoDeserializer}}; use yazi_codegen::FromLuaOwned; use yazi_macro::impl_data_any; -use crate::{auth::Auth, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; +use crate::{auth::{Auth, Domain}, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; #[derive(Clone, Eq, FromLuaOwned)] pub enum UrlBuf { Regular(LocBuf), Search { loc: LocBuf, auth: Arc }, Mount { loc: LocBuf, auth: Arc }, + Hub { loc: LocBuf, auth: Arc }, Scope { loc: LocBuf, auth: Arc }, Sftp { loc: LocBuf, auth: Arc }, } @@ -33,6 +34,7 @@ impl From> for UrlBuf { Url::Regular(loc) => Self::Regular(loc.into()), Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, + Url::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth: auth.clone() }, Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, } @@ -140,6 +142,7 @@ impl UrlBuf { Self::Regular(loc) => loc.into_inner().into(), Self::Search { loc, .. } => loc.into_inner().into(), Self::Mount { loc, .. } => loc.into_inner().into(), + Self::Hub { loc, .. } => loc.into_inner().into(), Self::Scope { loc, .. } => loc.into_inner().into(), Self::Sftp { loc, .. } => loc.into_inner().into(), } @@ -156,6 +159,7 @@ impl UrlBuf { Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Mount { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Hub { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Scope { loc, .. } => loc.try_set_name(name.encoded_bytes())?, Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?, }) @@ -166,6 +170,7 @@ impl UrlBuf { Self::Regular(loc) => Self::Regular(loc.rebase(base)), Self::Search { loc, auth } => Self::Search { loc: loc.rebase(base), auth: auth.clone() }, Self::Mount { loc, auth } => Self::Mount { loc: loc.rebase(base), auth: auth.clone() }, + Self::Hub { .. } => todo!(), Self::Scope { .. } => todo!(), Self::Sftp { .. } => todo!(), } @@ -188,6 +193,18 @@ impl UrlBuf { auth: Auth::search(query.as_ref()), }) } + + pub fn into_domain<'a>(mut self, domain: impl Into>) -> Self { + match &mut self { + Self::Regular(_) => {} + Self::Search { auth, .. } + | Self::Mount { auth, .. } + | Self::Hub { auth, .. } + | Self::Scope { auth, .. } + | Self::Sftp { auth, .. } => Arc::make_mut(auth).domain = domain.into().into_owned(), + } + self + } } impl Debug for UrlBuf { @@ -265,10 +282,14 @@ impl<'de> IntoDeserializer<'de, de::value::Error> for &'de UrlBuf { // --- Tests #[cfg(test)] mod tests { + use std::fmt::Debug; + use anyhow::Result; use super::*; - use crate::url::UrlLike; + use crate::{path::PathKind, url::UrlLike}; + + fn debug(value: impl Debug) -> String { format!("{value:?}").replace(r"\", "/") } #[test] fn test_join() -> anyhow::Result<()> { @@ -293,13 +314,7 @@ mod tests { for (base, path, expected) in cases { let base: UrlBuf = base.parse()?; - #[cfg(unix)] - assert_eq!(format!("{:?}", base.try_join(path)?), expected); - #[cfg(windows)] - assert_eq!( - format!("{:?}", base.try_join(path)?).replace(r"\", "/"), - expected.replace(r"\", "/") - ); + assert_eq!(debug(base.try_join(path)?), expected); } Ok(()) @@ -312,6 +327,8 @@ mod tests { // Regular ("/a", Some("/")), ("/", None), + ("a", Some("")), + ("", None), // Search ("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")), ("search://kw:1:1//a/b", Some("search://kw//a")), @@ -329,13 +346,16 @@ mod tests { ("sftp://vps//", None), // Relative ("search://kw:2:2/a/b", Some("search://kw:1:1/a")), - ("search://kw:1:1/a", None), + ("search://kw:1:1/a", Some("search://kw/")), ("search://kw/", None), + ("test-mount://7z:1:1/a", Some("test-mount://7z/")), + ("test-scope://aws/a", Some("test-scope://aws/")), + ("sftp://vps/a", Some("sftp://vps/")), ]; for (path, expected) in cases { let path: UrlBuf = path.parse()?; - assert_eq!(path.parent().map(|u| format!("{u:?}")).as_deref(), expected); + assert_eq!(path.parent().map(debug).as_deref(), expected); } Ok(()) @@ -344,29 +364,166 @@ mod tests { #[test] fn test_into_search() -> Result<()> { crate::init_tests(); - const S: char = std::path::MAIN_SEPARATOR; let u: UrlBuf = "/root".parse()?; - assert_eq!(format!("{u:?}"), "/root"); + assert_eq!(debug(&u), "/root"); let u = u.into_search("kw")?; - assert_eq!(format!("{u:?}"), "search://kw//root"); - assert_eq!(format!("{:?}", u.parent().unwrap()), "/"); + assert_eq!(debug(&u), "search://kw//root"); + assert_eq!(debug(u.parent().unwrap()), "/"); let u = u.try_join("examples")?; - assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); + assert_eq!(debug(&u), "search://kw:1:1//root/examples"); let u = u.try_join("README.md")?; - assert_eq!(format!("{u:?}"), format!("search://kw:2:2//root{S}examples{S}README.md")); + assert_eq!(debug(&u), "search://kw:2:2//root/examples/README.md"); let u = u.parent().unwrap(); - assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); + assert_eq!(debug(u), "search://kw:1:1//root/examples"); let u = u.parent().unwrap(); - assert_eq!(format!("{u:?}"), "search://kw//root"); + assert_eq!(debug(u), "search://kw//root"); let u = u.parent().unwrap(); - assert_eq!(format!("{u:?}"), "/"); + assert_eq!(debug(u), "/"); + + Ok(()) + } + + #[test] + fn test_hub_parse() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + assert_eq!(debug(&root), "test-hub://root/@/"); + assert_eq!(root.loc().kind(), PathKind::Os); + + let encoded: UrlBuf = "test-hub://%252C/@/".parse()?; + assert_eq!(debug(encoded), "test-hub://%252C/@/"); + let encoded: UrlBuf = "test-hub://b1/@a%2Cb%40c%25d%2Fe,root/foo/bar".parse()?; + assert_eq!(debug(encoded), "test-hub://b1/@a%2Cb%40c%25d%2Fe,root/foo/bar"); + + Ok(()) + } + + #[test] + fn test_hub_domain() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + let foo = root.try_join("foo")?.into_domain("a1"); + assert_eq!(debug(&foo), "test-hub://a1/@root/foo"); + + let bar = foo.try_join("bar")?.into_domain("b1"); + assert_eq!(bar.entry_key(), "b1"); + assert_eq!(debug(&bar), "test-hub://b1/@a1,root/foo/bar"); + assert_eq!(debug(bar.parent().unwrap()), "test-hub://a1/@root/foo"); + assert_eq!(debug(bar.parent().unwrap().parent().unwrap()), "test-hub://root/@/"); + + let relative = UrlCow::try_from("test-hub://a1/@/@abc")?; + assert_eq!(debug(relative.as_url()), "test-hub://a1/@/@abc"); + assert_eq!(debug(relative.parent().unwrap().as_url()), "test-hub:///@/"); + assert_eq!(relative.entry_key(), "a1"); + assert!(!relative.is_owned()); + assert!(relative.parent().unwrap().entry_key().is_empty()); + + Ok(()) + } + + #[test] + fn test_hub_join() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + let bar: UrlBuf = "test-hub://b1/@a1,root/foo/bar".parse()?; + + assert_eq!(debug(bar.try_join(".")?), "test-hub://b1/@a1,root/foo/bar"); + assert_eq!(debug(bar.try_join("..")?), "test-hub:///@b1,a1,root/foo/bar/.."); + + assert_eq!(debug(bar.try_join("/x/y")?), "test-hub:///@,//x/y"); + assert_eq!(debug(root.try_join("../../..")?), "test-hub:///@,,root/../../.."); + + let absolute = root.try_join("/foo")?; + assert_eq!(debug(&absolute), "test-hub:///@//foo"); + let absolute = absolute.into_domain("a1"); + assert_eq!(debug(&absolute), "test-hub://a1/@//foo"); + assert_eq!(debug(absolute.parent().unwrap().try_join("..")?), "test-hub:///@//.."); + + Ok(()) + } + + #[test] + fn test_hub_ports() -> Result<()> { + crate::init_tests(); + + let ports: UrlBuf = "test-hub://b1:2:1/@a1,root/foo/bar".parse()?; + assert_eq!(debug(ports.base()), "test-hub://root/@/"); + assert_eq!(debug(ports.trail()), "test-hub://a1/@root/foo"); + + let ports: UrlBuf = "test-hub://b1:3:1/@a1,root//foo/bar".parse()?; + assert_eq!(debug(ports.base()), "test-hub://root/@/"); + assert_eq!(debug(ports.trail()), "test-hub://a1/@root//foo"); + + let zeroed: UrlBuf = "test-hub://b1:0:0/@a1,root/foo/bar".parse()?; + assert_eq!(debug(zeroed.base()), "test-hub://b1/@a1,root/foo/bar"); + assert_eq!(debug(zeroed.trail()), "test-hub://b1/@a1,root/foo/bar"); + + Ok(()) + } + + #[test] + fn test_hub_invalid() { + crate::init_tests(); + + assert!("test-hub://a1/foo".parse::().is_err()); + assert!("test-hub://b1/@a1/foo/bar".parse::().is_err()); + } + + #[test] + fn test_hub_replace() -> Result<()> { + crate::init_tests(); + + let url: UrlBuf = "test-hub://b1/@a1,root/foo/bar".parse()?; + assert_eq!( + debug(url.try_replace(2, Path::new("baz/qux"))?.as_url()), + "test-hub://b1:2:2/@,a1,root/foo/baz/qux" + ); + assert_eq!(debug(url.try_replace(1, Path::new("qux"))?.as_url()), "test-hub://b1/@root/qux"); + + Ok(()) + } + + #[cfg(windows)] + #[test] + fn test_hub_windows() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + let c = root.try_join(r"C:\")?.into_domain("c-root"); + assert_eq!(c.entry_key(), "c-root"); + assert_eq!(c.auth().parent_depth(), 0); + + let drive = c.try_join(r"Users\file.txt")?.into_domain("file"); + assert_eq!(drive.loc(), Path::new(r"C:\Users\file.txt")); + assert_eq!(drive.auth().parent_depth(), 2); + + let parent = drive.parent().unwrap(); + assert_eq!(parent.loc(), Path::new(r"C:\Users")); + + let parent = parent.parent().unwrap(); + assert_eq!(parent.loc(), Path::new(r"C:\")); + assert_eq!(parent.entry_key(), "c-root"); + assert!(parent.parent().is_none()); + + let relative = root.try_join(r"C:foo")?; + assert!(!relative.is_absolute()); + assert_eq!(relative.parent().unwrap().loc(), Path::new("C:")); + assert!(relative.parent().unwrap().parent().is_none()); + + let unc = root.try_join(r"\\server\share\dir\file")?; + assert!(unc.is_absolute()); + assert_eq!(unc.loc(), Path::new(r"\\server\share\dir\file")); + assert_eq!(unc.auth().parent_depth(), 2); Ok(()) } diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs index ed7f2cfe..5bdaf627 100644 --- a/yazi-shared/src/url/components.rs +++ b/yazi-shared/src/url/components.rs @@ -1,11 +1,12 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not}; -use crate::{auth::Auth, loc::Loc, path, spec::{Encode as EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; +use crate::{auth::Auth, loc::Loc, path, spec::{EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; #[derive(Clone)] pub struct Components<'a> { inner: path::Components<'a>, url: Url<'a>, + auth_yields: usize, back_yields: usize, scheme_yielded: bool, } @@ -15,6 +16,7 @@ impl<'a> From> for Components<'a> { Self { inner: value.loc().components(), url: value, + auth_yields: 0, back_yields: 0, scheme_yielded: false, } @@ -83,6 +85,10 @@ impl<'a> Components<'a> { Url::Mount { auth, .. } => { Url::Mount { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), auth } } + Url::Hub { auth, .. } => Url::Hub { + loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), + auth: auth.parent_at(self.auth_yields), + }, Url::Scope { auth, .. } => { Url::Scope { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), auth } } @@ -116,6 +122,7 @@ impl<'a> Iterator for Components<'a> { impl<'a> DoubleEndedIterator for Components<'a> { fn next_back(&mut self) -> Option { if let Some(c) = self.inner.next_back() { + self.auth_yields += c.has_auth() as usize; self.back_yields += 1; Some(c.into()) } else if !self.scheme_yielded { diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 4b09a528..37f22647 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -1,16 +1,17 @@ use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf, sync::Arc}; -use anyhow::Result; +use anyhow::{Result, ensure}; use serde::{Deserialize, Deserializer, Serialize}; use typed_path::{UnixPath, UnixPathBuf}; -use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}}; +use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{DynPath, PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}}; #[derive(Clone, Debug)] pub enum UrlCow<'a> { Regular(LocCow<'a>), Search { loc: LocCow<'a>, auth: Arc }, Mount { loc: LocCow<'a>, auth: Arc }, + Hub { loc: LocCow<'a>, auth: Arc }, Scope { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc }, Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc }, } @@ -21,6 +22,7 @@ impl<'a> From> for UrlCow<'a> { Url::Regular(loc) => Self::Regular(loc.into()), Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, + Url::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth: auth.clone() }, Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, } @@ -40,6 +42,7 @@ impl From for UrlCow<'_> { UrlBuf::Regular(loc) => Self::Regular(loc.into()), UrlBuf::Search { loc, auth } => Self::Search { loc: loc.into(), auth }, UrlBuf::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth }, + UrlBuf::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth }, UrlBuf::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth }, UrlBuf::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth }, } @@ -113,10 +116,13 @@ impl<'a> TryFrom<(Spec, PathDyn<'a>)> for UrlCow<'a> { fn try_from((spec, path): (Spec, PathDyn<'a>)) -> Result { let Spec { auth, uri, urn } = spec; + validate_auth_depth(&auth, path)?; + Ok(match auth.kind { AuthKind::Regular => Self::Regular(Loc::bare(path.as_os()?).into()), AuthKind::Search => Self::Search { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, AuthKind::Mount => Self::Mount { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, + AuthKind::Hub => Self::Hub { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, AuthKind::Scope => Self::Scope { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, AuthKind::Sftp => Self::Sftp { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, }) @@ -128,6 +134,8 @@ impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> { fn try_from((spec, path): (Spec, PathBufDyn)) -> Result { let Spec { auth, uri, urn } = spec; + validate_auth_depth(&auth, path.dyn_path())?; + Ok(match auth.kind { AuthKind::Regular => { Self::Regular(LocBuf::::from(path.into_os()?).into()) @@ -140,6 +148,9 @@ impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth, }, + AuthKind::Hub => { + Self::Hub { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth } + } AuthKind::Scope => { Self::Scope { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth } } @@ -172,6 +183,7 @@ impl<'a> UrlCow<'a> { Self::Regular(loc) => loc.is_owned(), Self::Search { loc, .. } => loc.is_owned(), Self::Mount { loc, .. } => loc.is_owned(), + Self::Hub { loc, .. } => loc.is_owned(), Self::Scope { loc, .. } => loc.is_owned(), Self::Sftp { loc, .. } => loc.is_owned(), } @@ -182,6 +194,7 @@ impl<'a> UrlCow<'a> { Self::Regular(loc) => UrlBuf::Regular(loc.into_owned()), Self::Search { loc, auth } => UrlBuf::Search { loc: loc.into_owned(), auth }, Self::Mount { loc, auth } => UrlBuf::Mount { loc: loc.into_owned(), auth }, + Self::Hub { loc, auth } => UrlBuf::Hub { loc: loc.into_owned(), auth }, Self::Scope { loc, auth } => UrlBuf::Scope { loc: loc.into_owned(), auth }, Self::Sftp { loc, auth } => UrlBuf::Sftp { loc: loc.into_owned(), auth }, } @@ -191,7 +204,9 @@ impl<'a> UrlCow<'a> { let (uri, urn) = Spec::retrieve_ports(self.as_url()); let (auth, path) = match self { Self::Regular(loc) => (Auth::default_arc(), loc.into_path()), - Self::Search { loc, auth } | Self::Mount { loc, auth } => (auth, loc.into_path()), + Self::Search { loc, auth } | Self::Mount { loc, auth } | Self::Hub { loc, auth } => { + (auth, loc.into_path()) + } Self::Scope { loc, auth } | Self::Sftp { loc, auth } => (auth, loc.into_path()), }; (Spec { auth, uri, urn }, path) @@ -222,6 +237,16 @@ impl<'de> Deserialize<'de> for UrlCow<'_> { } } +fn validate_auth_depth(auth: &Auth, path: PathDyn) -> Result<()> { + if auth.kind == AuthKind::Hub { + ensure!( + auth.parent_depth() == path.components().auth_depth(), + "Hub URL parent depth does not match its path" + ); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/yazi-shared/src/url/de.rs b/yazi-shared/src/url/de.rs index d3449501..db494024 100644 --- a/yazi-shared/src/url/de.rs +++ b/yazi-shared/src/url/de.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; -use crate::{data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; +use crate::{auth::Domain, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>); @@ -33,7 +33,7 @@ impl<'de, 'a: 'de> Deserializer<'de> for UrlDeserializer<'a> { struct MapDeserializer<'a> { kind: Option<&'static str>, scheme: Option>, - domain: Option>, + domain: Option>, uri: Option, urn: Option, path: Option>, @@ -47,7 +47,7 @@ impl<'a> MapDeserializer<'a> { Self { kind: Some(spec.kind.into()), scheme: Some(spec.scheme.intern()), - domain: Some(spec.domain.intern()), + domain: Some(spec.domain.clone()), uri: Some(uri), urn: Some(urn), path: Some(path.into_encoded_bytes()), diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index 85e43a76..82ecb7e7 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::{spec::Encode, url::Url}; +use crate::{spec::EncodeSpec, url::Url}; pub struct Display<'a>(pub Url<'a>); @@ -6,7 +6,7 @@ impl std::fmt::Display for Display<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (kind, loc) = (self.0.kind(), self.0.loc()); if kind.is_virtual() { - Encode(self.0).fmt(f)?; + EncodeSpec(self.0).fmt(f)?; } loc.display().fmt(f) } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 8969547d..52ef4191 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -1,24 +1,36 @@ use std::fmt::{self, Display}; -use percent_encoding::{CONTROLS, percent_encode}; +use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; -use crate::{auth::Encode as EncodeAuth, url::Url}; +use crate::{auth::{EncodeAuth, EncodePrefix}, spec::EncodeSpec, url::Url}; #[derive(Clone, Copy)] pub struct Encode<'a>(pub Url<'a>); +impl Encode<'_> { + pub fn loc(b: &[u8]) -> PercentEncode<'_> { + const SET: &AsciiSet = &CONTROLS.add(b'%'); + percent_encode(b, SET) + } +} + impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use crate::spec::Encode as E; - - let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS); + let loc = Self::loc(self.0.loc().encoded_bytes()); match self.0 { Url::Regular(_) => write!(f, "regular~://{loc}"), Url::Search { auth, .. } | Url::Mount { auth, .. } + | Url::Hub { auth, .. } | Url::Scope { auth, .. } | Url::Sftp { auth, .. } => { - write!(f, "{}{}/{loc}", EncodeAuth(auth, true), E::ports((*self).into())) + write!( + f, + "{}{}{}{loc}", + EncodeAuth(auth, true), + EncodeSpec::ports((*self).into()), + EncodePrefix(auth) + ) } } } diff --git a/yazi-shared/src/url/like.rs b/yazi-shared/src/url/like.rs index 471c808b..ccaaebb9 100644 --- a/yazi-shared/src/url/like.rs +++ b/yazi-shared/src/url/like.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, path::Path}; use anyhow::Result; -use crate::{auth::{Auth, AuthKind}, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; +use crate::{auth::{Auth, AuthKind}, path::{DynPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; pub trait UrlLike where @@ -22,6 +22,8 @@ where fn ext(&self) -> Option> { self.as_url().ext() } + fn entry_key(&self) -> PathDyn<'_> { self.as_url().entry_key() } + fn has_base(&self) -> bool { self.as_url().has_base() } fn has_root(&self) -> bool { self.as_url().has_root() } @@ -44,6 +46,8 @@ where fn pair(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair() } + fn pair2(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair2() } + fn parent(&self) -> Option> { self.as_url().parent() } fn spec(&self) -> Spec { self.as_url().spec() } @@ -62,8 +66,8 @@ where self.as_url().try_join(path) } - fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result> { - self.as_url().try_replace(take, path) + fn try_replace<'a>(&self, take: usize, to: impl DynPathRef<'a>) -> Result> { + self.as_url().try_replace(take, to) } fn try_starts_with(&self, base: impl AsUrl) -> Result { diff --git a/yazi-shared/src/url/lua.rs b/yazi-shared/src/url/lua.rs index 65ef2086..2ad5777c 100644 --- a/yazi-shared/src/url/lua.rs +++ b/yazi-shared/src/url/lua.rs @@ -121,6 +121,9 @@ impl UserData for UrlBuf { methods.add_method_once("into_search", |_, me, domain: LuaString| { me.into_search(domain.to_str()?).into_lua_err() }); + methods.add_method_once("into_domain", |_, me, domain: LuaString| { + Ok(me.into_domain(domain.to_str()?.to_owned())) + }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other)); methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index 547b5824..c5a30b76 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -42,6 +42,7 @@ impl AsUrl for UrlBuf { Self::Regular(loc) => Url::Regular(loc.as_loc()), Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, + Self::Hub { loc, auth } => Url::Hub { loc: loc.as_loc(), auth }, Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, } @@ -64,6 +65,7 @@ impl AsUrl for UrlCow<'_> { Self::Regular(loc) => Url::Regular(loc.as_loc()), Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, + Self::Hub { loc, auth } => Url::Hub { loc: loc.as_loc(), auth }, Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 07fdbb8c..db3d1dbe 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -5,13 +5,14 @@ use hashbrown::Equivalent; use serde::Serialize; use super::Encode as EncodeUrl; -use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{Encode as EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; +use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{DynPath, DynPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub enum Url<'a> { Regular(Loc<'a>), Search { loc: Loc<'a>, auth: &'a Arc }, Mount { loc: Loc<'a>, auth: &'a Arc }, + Hub { loc: Loc<'a>, auth: &'a Arc }, Scope { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc }, Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc }, } @@ -60,11 +61,22 @@ impl<'a> Url<'a> { Self::Regular(_) => &Auth::DEFAULT, Self::Search { auth, .. } | Self::Mount { auth, .. } + | Self::Hub { auth, .. } | Self::Scope { auth, .. } | Self::Sftp { auth, .. } => auth, } } + fn auth_at(self, base: Self) -> Option<&'a Auth> { + let (Self::Hub { auth, .. }, Self::Hub { .. }) = (self, base) else { + return self.auth().covariant(base.auth()).then_some(self.auth()); + }; + + let depth = + self.loc().components().auth_depth().checked_sub(base.loc().components().auth_depth())?; + Some(auth.parent_at(depth)) + } + #[inline] pub fn as_regular(self) -> Result { Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) @@ -75,6 +87,10 @@ impl<'a> Url<'a> { Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), Self::Search { loc, auth } => Self::Search { loc: Loc::zeroed(loc.base()), auth }, Self::Mount { loc, auth } => Self::Mount { loc: Loc::zeroed(loc.base()), auth }, + Self::Hub { loc, auth } => Self::Hub { + loc: Loc::bare(loc.base()), + auth: auth.parent_at(loc.uri().dyn_path().components().auth_depth()), + }, Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.base()), auth }, Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.base()), auth }, } @@ -95,6 +111,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.extension()?.as_strand(), Self::Search { loc, .. } => loc.extension()?.as_strand(), Self::Mount { loc, .. } => loc.extension()?.as_strand(), + Self::Hub { loc, .. } => loc.extension()?.as_strand(), Self::Scope { loc, .. } => loc.extension()?.as_strand(), Self::Sftp { loc, .. } => loc.extension()?.as_strand(), }) @@ -106,6 +123,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.has_base(), Self::Search { loc, .. } => loc.has_base(), Self::Mount { loc, .. } => loc.has_base(), + Self::Hub { loc, .. } => loc.has_base(), Self::Scope { loc, .. } => loc.has_base(), Self::Sftp { loc, .. } => loc.has_base(), } @@ -120,6 +138,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.has_trail(), Self::Search { loc, .. } => loc.has_trail(), Self::Mount { loc, .. } => loc.has_trail(), + Self::Hub { loc, .. } => loc.has_trail(), Self::Scope { loc, .. } => loc.has_trail(), Self::Sftp { loc, .. } => loc.has_trail(), } @@ -140,11 +159,12 @@ impl<'a> Url<'a> { #[inline] pub fn loc(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.as_path(), - Self::Search { loc, .. } => loc.as_path(), - Self::Mount { loc, .. } => loc.as_path(), - Self::Scope { loc, .. } => loc.as_path(), - Self::Sftp { loc, .. } => loc.as_path(), + Self::Regular(loc) => loc.dyn_path(), + Self::Search { loc, .. } => loc.dyn_path(), + Self::Mount { loc, .. } => loc.dyn_path(), + Self::Hub { loc, .. } => loc.dyn_path(), + Self::Scope { loc, .. } => loc.dyn_path(), + Self::Sftp { loc, .. } => loc.dyn_path(), } } @@ -154,6 +174,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.file_name()?.as_strand(), Self::Search { loc, .. } => loc.file_name()?.as_strand(), Self::Mount { loc, .. } => loc.file_name()?.as_strand(), + Self::Hub { loc, .. } => loc.file_name()?.as_strand(), Self::Scope { loc, .. } => loc.file_name()?.as_strand(), Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), }) @@ -165,6 +186,19 @@ impl<'a> Url<'a> { #[inline] pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } + #[inline] + pub fn pair2(self) -> Option<(Self, PathDyn<'a>)> { + Some((self.parent()?, Some(self.entry_key()).filter(|k| !k.is_empty())?)) + } + + #[inline] + pub fn entry_key(self) -> PathDyn<'a> { + match self { + Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(auth.domain.as_bytes())), + _ => self.urn(), + } + } + pub fn parent(self) -> Option { let uri = self.uri(); @@ -187,6 +221,11 @@ impl<'a> Url<'a> { Self::Mount { loc: Loc::floated(loc.parent()?, loc.base()), auth } } + // Hub + Self::Hub { loc, auth } => { + Self::Hub { loc: Loc::bare(loc.parent()?), auth: auth.parent.as_ref()? } + } + // Scope Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.parent()?), auth }, @@ -205,6 +244,7 @@ impl<'a> Url<'a> { Self::Regular(_) => Auth::default_arc(), Self::Search { auth, .. } | Self::Mount { auth, .. } + | Self::Hub { auth, .. } | Self::Scope { auth, .. } | Self::Sftp { auth, .. } => auth.clone(), }; @@ -219,6 +259,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.file_stem()?.as_strand(), Self::Search { loc, .. } => loc.file_stem()?.as_strand(), Self::Mount { loc, .. } => loc.file_stem()?.as_strand(), + Self::Hub { loc, .. } => loc.file_stem()?.as_strand(), Self::Scope { loc, .. } => loc.file_stem()?.as_strand(), Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), }) @@ -253,6 +294,11 @@ impl<'a> Url<'a> { Self::Mount { loc: Loc::new(loc.trail(), loc.base(), loc.base()), auth } } + Self::Hub { loc, auth } => Self::Hub { + loc: Loc::bare(loc.trail()), + auth: auth.parent_at(loc.urn().dyn_path().components().auth_depth()), + }, + Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.trail()), auth }, Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.trail()), auth }, @@ -261,13 +307,16 @@ impl<'a> Url<'a> { pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { match self { - Self::Regular(loc) | Self::Search { loc, .. } | Self::Mount { loc, .. } => { + Self::Regular(loc) + | Self::Search { loc, .. } + | Self::Mount { loc, .. } + | Self::Hub { loc, .. } => { let (base, rest, urn) = loc.triple(); - (base.as_path(), rest.as_path(), urn.as_path()) + (base.dyn_path(), rest.dyn_path(), urn.dyn_path()) } Self::Scope { loc, .. } | Self::Sftp { loc, .. } => { let (base, rest, urn) = loc.triple(); - (base.as_path(), rest.as_path(), urn.as_path()) + (base.dyn_path(), rest.dyn_path(), urn.dyn_path()) } } } @@ -279,6 +328,7 @@ impl<'a> Url<'a> { } pub fn try_join(self, path: impl AsStrand) -> Result { + let path = path.as_strand(); let joined = self.loc().try_join(path)?; Ok(match self { @@ -300,6 +350,11 @@ impl<'a> Url<'a> { UrlBuf::Mount { loc: LocBuf::::zeroed(joined.into_os()?), auth: auth.clone() } } + Self::Hub { auth, .. } => UrlBuf::Hub { + loc: joined.into_os()?.into(), + auth: auth.clone().descend(path.as_os_path()?), + }, + Self::Scope { auth, .. } => { UrlBuf::Scope { loc: joined.into_unix()?.into(), auth: auth.clone() } } @@ -310,8 +365,8 @@ impl<'a> Url<'a> { }) } - pub fn try_replace<'b>(self, take: usize, to: impl AsPathRef<'b>) -> Result> { - self.try_replace_impl(take, to.as_path_ref()) + pub fn try_replace<'b>(self, take: usize, to: impl DynPathRef<'b>) -> Result> { + self.try_replace_impl(take, to.dyn_path_ref()) } fn try_replace_impl<'b>(self, take: usize, rep: PathDyn<'b>) -> Result> { @@ -337,6 +392,10 @@ impl<'a> Url<'a> { loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), auth: auth.clone(), }, + Self::Hub { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Hub { + auth: auth.clone().with_parent_depth(path.components().auth_depth()), + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + }, Self::Scope { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Scope { loc: LocBuf::::new(path.into_unix()?, loc.base(), loc.trail()), auth: auth.clone(), @@ -354,6 +413,10 @@ impl<'a> Url<'a> { loc: LocBuf::::saturated(path.into_os()?, self.kind()), auth: auth.clone(), }, + Self::Hub { auth, .. } => UrlBuf::Hub { + auth: auth.clone().with_parent_depth(path.components().auth_depth()), + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + }, Self::Scope { auth, .. } => UrlBuf::Scope { loc: LocBuf::::saturated(path.into_unix()?, self.kind()), auth: auth.clone(), @@ -367,10 +430,12 @@ impl<'a> Url<'a> { Ok(url.into()) } - #[inline] pub fn try_starts_with(self, base: impl AsUrl) -> Result { let base = base.as_url(); - Ok(self.loc().try_starts_with(base.loc())? && self.auth().covariant(base.auth())) + Ok( + self.loc().try_starts_with(base.loc())? + && self.auth_at(base).is_some_and(|a| a == base.auth()), + ) } pub fn try_strip_prefix(self, base: impl AsUrl) -> Result, StripPrefixError> { @@ -379,7 +444,7 @@ impl<'a> Url<'a> { let base = base.as_url(); let prefix = self.loc().try_strip_prefix(base.loc())?; - if self.auth().covariant(base.auth()) { + if self.auth_at(base).is_some_and(|a| a == base.auth()) { return Ok(prefix); } @@ -420,22 +485,24 @@ impl<'a> Url<'a> { #[inline] pub fn uri(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.uri().as_path(), - Self::Search { loc, .. } => loc.uri().as_path(), - Self::Mount { loc, .. } => loc.uri().as_path(), - Self::Scope { loc, .. } => loc.uri().as_path(), - Self::Sftp { loc, .. } => loc.uri().as_path(), + Self::Regular(loc) => loc.uri().dyn_path(), + Self::Search { loc, .. } => loc.uri().dyn_path(), + Self::Mount { loc, .. } => loc.uri().dyn_path(), + Self::Hub { loc, .. } => loc.uri().dyn_path(), + Self::Scope { loc, .. } => loc.uri().dyn_path(), + Self::Sftp { loc, .. } => loc.uri().dyn_path(), } } #[inline] pub fn urn(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.urn().as_path(), - Self::Search { loc, .. } => loc.urn().as_path(), - Self::Mount { loc, .. } => loc.urn().as_path(), - Self::Scope { loc, .. } => loc.urn().as_path(), - Self::Sftp { loc, .. } => loc.urn().as_path(), + Self::Regular(loc) => loc.urn().dyn_path(), + Self::Search { loc, .. } => loc.urn().dyn_path(), + Self::Mount { loc, .. } => loc.urn().dyn_path(), + Self::Hub { loc, .. } => loc.urn().dyn_path(), + Self::Scope { loc, .. } => loc.urn().dyn_path(), + Self::Sftp { loc, .. } => loc.urn().dyn_path(), } } } diff --git a/yazi-vfs/src/engine/demand.rs b/yazi-vfs/src/engine/demand.rs index ecbbf140..02120a53 100644 --- a/yazi-vfs/src/engine/demand.rs +++ b/yazi-vfs/src/engine/demand.rs @@ -40,7 +40,7 @@ impl FileBuilder for Demand { AuthKind::Regular | AuthKind::Search => { self.0.build::().open(url).await?.into() } - AuthKind::Mount | AuthKind::Scope => { + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope => { self.0.build::().open(url).await?.into() } AuthKind::Sftp => self.0.build::().open(url).await?.into(), diff --git a/yazi-vfs/src/engine/engine.rs b/yazi-vfs/src/engine/engine.rs index 9a9ce8e9..b727b682 100644 --- a/yazi-vfs/src/engine/engine.rs +++ b/yazi-vfs/src/engine/engine.rs @@ -55,7 +55,7 @@ where match (from.kind().is_local(), to.kind().is_local()) { (true, true) => Local::new(from).await?.copy(to.loc(), attrs).await, - (false, false) if from.auth().covariant(to.auth()) => { + (false, false) if from.auth().same_service(to.auth()) => { Engines::new(from).await?.copy(to.loc(), attrs).await } (true, false) | (false, true) | (false, false) => super::copy_impl(from, to, attrs).await, @@ -75,7 +75,7 @@ where let (from, to) = (from.as_url(), to.as_url()); let attrs = attrs.into(); - if from.auth().covariant(to.auth()) { + if from.auth().same_service(to.auth()) { let engine = Engines::new(from).await?; if engine.capabilities().await?.copy_progressive { return engine.copy_progressive(to.loc(), attrs); @@ -119,7 +119,7 @@ where V: AsUrl, { let (original, link) = (original.as_url(), link.as_url()); - if original.auth().covariant(link.auth()) { + if original.auth().same_service(link.auth()) { Engines::new(original).await?.hard_link(link.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) @@ -208,7 +208,7 @@ where V: AsUrl, { let (from, to) = (from.as_url(), to.as_url()); - if from.auth().covariant(to.auth()) { + if from.auth().same_service(to.auth()) { Engines::new(from).await?.rename(to.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) @@ -268,7 +268,9 @@ where let url = url.into(); match url.as_url() { Url::Regular(_) | Url::Search { .. } => yazi_fs::engine::local::try_absolute(url), - Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => super::try_absolute_impl(url), + Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => { + super::try_absolute_impl(url) + } } } diff --git a/yazi-vfs/src/engine/engines.rs b/yazi-vfs/src/engine/engines.rs index cace26f6..36a8d722 100644 --- a/yazi-vfs/src/engine/engines.rs +++ b/yazi-vfs/src/engine/engines.rs @@ -2,7 +2,7 @@ use std::io; use tokio::sync::mpsc; use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}}; -use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] pub(super) enum Engines<'a> { @@ -52,7 +52,7 @@ impl<'a> Engine for Engines<'a> { async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPath, + P: DynPath, { match self { Self::Local(p) => p.copy(to, attrs).await, @@ -63,7 +63,7 @@ impl<'a> Engine for Engines<'a> { fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { match self { @@ -107,7 +107,7 @@ impl<'a> Engine for Engines<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { match self { Self::Local(p) => p.hard_link(to).await, @@ -129,7 +129,7 @@ impl<'a> Engine for Engines<'a> { Ok(match url.kind() { K::Regular | K::Search => Self::Me::Local(yazi_fs::engine::local::Local::new(url).await?), - K::Mount | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?), + K::Mount | K::Hub | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?), K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?), }) } @@ -184,7 +184,7 @@ impl<'a> Engine for Engines<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { match self { Self::Local(p) => p.rename(to).await, diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs index e9810f63..81373ec9 100644 --- a/yazi-vfs/src/engine/lua/lua.rs +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -6,7 +6,7 @@ use yazi_binding::MpscTx; use yazi_config::vfs::{ServiceLua, Vfs}; use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files}; use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}}; -use yazi_shared::{event::Cmd, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{event::Cmd, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; use crate::engine::lua::ReadDir; @@ -48,23 +48,23 @@ impl<'a> Engine for Lua<'a> { async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPath, + P: DynPath, { let from = self.url.to_owned(); - let to = to.as_path().to_owned(); + let to = to.dyn_path().to_owned(); Ok(self.call(ProviderJob::Copy { from, to, attrs }).await?.0?) } fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { let (tx, rx) = mpsc::channel(20); let job = ProviderJob::CopyProgressive { from: self.url.to_owned(), - to: to.as_path().to_owned(), + to: to.dyn_path().to_owned(), attrs: attrs.into(), tx: MpscTx::map(tx.clone(), Ok), }; @@ -88,10 +88,10 @@ impl<'a> Engine for Lua<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { let from = self.url.to_owned(); - let to = to.as_path().to_owned(); + let to = to.dyn_path().to_owned(); Ok(self.call(ProviderJob::HardLink { from, to }).await?.ok()?) } @@ -103,7 +103,7 @@ impl<'a> Engine for Lua<'a> { } async fn new<'b>(url: Url<'b>) -> io::Result> { - let (Url::Mount { auth, .. } | Url::Scope { auth, .. }) = url else { + let (Url::Mount { auth, .. } | Url::Hub { auth, .. } | Url::Scope { auth, .. }) = url else { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Not a custom VFS URL: {url:?}"), @@ -141,10 +141,10 @@ impl<'a> Engine for Lua<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { let from = self.url.to_owned(); - let to = to.as_path().to_owned(); + let to = to.dyn_path().to_owned(); Ok(self.call(ProviderJob::Rename { from, to }).await?.ok()?) } diff --git a/yazi-vfs/src/engine/sftp/sftp.rs b/yazi-vfs/src/engine/sftp/sftp.rs index fca991df..303f1639 100644 --- a/yazi-vfs/src/engine/sftp/sftp.rs +++ b/yazi-vfs/src/engine/sftp/sftp.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::engine::{Capabilities, DirReader, Engine, FileHolder}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; use crate::engine::sftp::Conn; @@ -83,9 +83,9 @@ impl<'a> Engine for Sftp<'a> { async fn copy

(&self, to: P, attrs: yazi_fs::engine::Attrs) -> io::Result where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_unix()?; + let to = to.dyn_path().as_unix()?; let attrs = super::Attrs(attrs).try_into().unwrap_or_default(); let op = self.op().await?; @@ -107,12 +107,12 @@ impl<'a> Engine for Sftp<'a> { fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { let to = UrlBuf::Sftp { loc: LocBuf::::saturated( - to.as_path().to_unix_owned()?, + to.dyn_path().to_unix_owned()?, AuthKind::Sftp, ), auth: self.config.auth.clone(), @@ -138,9 +138,9 @@ impl<'a> Engine for Sftp<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_unix()?; + let to = to.dyn_path().as_unix()?; Ok(self.op().await?.hardlink(self.path, to).await?) } @@ -176,9 +176,9 @@ impl<'a> Engine for Sftp<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_unix()?; + let to = to.dyn_path().as_unix()?; let op = self.op().await?; match op.rename_posix(self.path, &to).await { diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 6c817473..131e0af7 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -97,20 +97,20 @@ impl Local { let mut ops = Vec::with_capacity(urls.len()); for u in urls { - let Some((parent, urn)) = u.pair() else { continue }; + let Some((parent, key)) = u.pair2() else { continue }; let Ok(file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; }; if let Some(p) = file.url.as_local() && !engine::local::match_name_case(p).await { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; } - ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); } FilesOp::mutate(ops); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/remote/remote.rs index 336f470f..6a94b9d1 100644 --- a/yazi-watcher/src/remote/remote.rs +++ b/yazi-watcher/src/remote/remote.rs @@ -39,16 +39,16 @@ impl Remote { let mut ups = Vec::with_capacity(urls.len()); for (u, upload) in urls { - let Some((parent, urn)) = u.pair() else { continue }; + let Some((parent, key)) = u.pair2() else { continue }; let Ok(mut file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; }; let is_file = file.is_file(); file.cha.ctime = Some(SystemTime::now()); - ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); if upload && is_file { ups.push(u); } diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 1c4c5336..97718f8d 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -21,7 +21,7 @@ impl Reporter { for url in urls.into_iter().map(Into::into) { match url.as_url().kind() { AuthKind::Regular | AuthKind::Search => self.report_local(url), - AuthKind::Mount => {} // TODO: mounted VFS cache invalidation + AuthKind::Mount | AuthKind::Hub => {} // TODO: mounted VFS cache invalidation AuthKind::Scope | AuthKind::Sftp => self.report_remote(url), } } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index 6628191b..2c035378 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -59,7 +59,7 @@ impl Watched { AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth }, AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth }, AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth }, - AuthKind::Regular | AuthKind::Search => return None, + AuthKind::Regular | AuthKind::Search | AuthKind::Hub => return None, }; if self.contains_url(&url) { Some(url) } else { None } } From 60d94608cb050f68c5ce85b7e236f0e5b121538d Mon Sep 17 00:00:00 2001 From: Omenabaka <592695+Omenabaka@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:58:16 +0300 Subject: [PATCH 09/12] fix: magick preview of multiframe files (#4024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲雅 misaki masa --- yazi-plugin/preset/plugins/magick.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yazi-plugin/preset/plugins/magick.lua b/yazi-plugin/preset/plugins/magick.lua index 15993771..77fb2b4d 100644 --- a/yazi-plugin/preset/plugins/magick.lua +++ b/yazi-plugin/preset/plugins/magick.lua @@ -25,9 +25,11 @@ function M:preload(job) return true end - local cmd = M.with_limit():arg(tostring(job.file.path)) + local cmd = M.with_limit():arg { "-define", "filename:literal=true" } if job.args.flatten then - cmd:arg("-flatten") + cmd:arg { tostring(job.file.path), "-flatten" } + else + cmd:arg { "-define", "image:frames=0", tostring(job.file.path) } end cmd:arg { "-auto-orient", "-strip" } From 6aa75594dbea287001c823853235e6a4f9d45997 Mon Sep 17 00:00:00 2001 From: nabsei <166228107+nabsei@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:59:20 +0200 Subject: [PATCH 10/12] docs: add a note about following the contribution guidelines in the PR template (#4140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲雅 misaki masa --- .github/pull_request_template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a0e4288f..baacd258 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,3 +20,5 @@ If it has already been detailed in the associated issue, please skip this sectio - [ ] I have read [CONTRIBUTING.md](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md) - [ ] I confirm this PR follows the [AI Policy](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md#ai-policy) + + From 58f1013348c969538352e3105f58e8d8e870a092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 22 Jul 2026 03:13:58 +0800 Subject: [PATCH 11/12] feat: trash bin (#4144) --- .github/workflows/validate-issue.yml | 2 +- .github/workflows/validate-pr.yml | 4 +- CHANGELOG.md | 2 + Cargo.lock | 381 +++++++++++++-------- Cargo.toml | 27 +- yazi-actor/src/mgr/bulk_create.rs | 12 +- yazi-actor/src/mgr/bulk_rename.rs | 17 +- yazi-actor/src/mgr/cd.rs | 6 +- yazi-actor/src/mgr/create.rs | 2 +- yazi-actor/src/mgr/download.rs | 16 +- yazi-actor/src/mgr/follow.rs | 2 +- yazi-actor/src/mgr/hidden.rs | 7 +- yazi-actor/src/mgr/open.rs | 5 +- yazi-actor/src/mgr/open_do.rs | 18 +- yazi-actor/src/mgr/peek.rs | 18 +- yazi-actor/src/mgr/refresh.rs | 40 +-- yazi-actor/src/mgr/rename.rs | 2 +- yazi-actor/src/mgr/search.rs | 4 +- yazi-actor/src/mgr/sort.rs | 7 +- yazi-actor/src/mgr/watch.rs | 8 +- yazi-binding/src/macros.rs | 14 +- yazi-boot/Cargo.toml | 2 +- yazi-cli/Cargo.toml | 3 +- yazi-cli/src/env/env.rs | 4 +- yazi-codegen/Cargo.toml | 4 +- yazi-config/Cargo.toml | 1 + yazi-config/preset/keymap-default.toml | 5 +- yazi-config/preset/vfs-default.toml | 3 + yazi-config/preset/yazi-default.toml | 8 + yazi-config/src/pattern.rs | 19 +- yazi-core/src/mgr/snap.rs | 27 +- yazi-core/src/mgr/yanked.rs | 4 +- yazi-core/src/tab/folder.rs | 34 +- yazi-core/src/tab/preview.rs | 44 +-- yazi-core/src/tab/snap.rs | 11 +- yazi-core/src/tasks/prework.rs | 6 +- yazi-ffi/Cargo.toml | 3 + yazi-ffi/src/com.rs | 16 + yazi-ffi/src/lib.rs | 3 + yazi-fm/src/input/input.rs | 6 +- yazi-fs/Cargo.toml | 5 +- yazi-fs/src/auth.rs | 43 +++ yazi-fs/src/cha/cha.rs | 10 + yazi-fs/src/cha/cha_sig.rs | 21 ++ yazi-fs/src/cha/lua.rs | 10 +- yazi-fs/src/cha/mod.rs | 2 +- yazi-fs/src/cwd.rs | 6 +- yazi-fs/src/engine/local/dir_entry.rs | 17 +- yazi-fs/src/engine/traits.rs | 30 +- yazi-fs/src/entries.rs | 23 +- yazi-fs/src/file/file.rs | 43 ++- yazi-fs/src/file/file_extra.rs | 67 ++++ yazi-fs/src/file/file_sig.rs | 26 ++ yazi-fs/src/file/lua.rs | 6 +- yazi-fs/src/file/mod.rs | 2 +- yazi-fs/src/hash.rs | 52 ++- yazi-fs/src/lib.rs | 4 +- yazi-fs/src/op.rs | 16 +- yazi-fs/src/path/mod.rs | 2 +- yazi-fs/src/path/percent.rs | 40 --- yazi-fs/src/spec.rs | 35 -- yazi-fs/src/splatter.rs | 125 ++++--- yazi-fs/src/trash/entry.rs | 53 +++ yazi-fs/src/trash/freedesktop.rs | 171 +++++++++ yazi-fs/src/trash/lua.rs | 73 ++++ yazi-fs/src/trash/macos.rs | 188 ++++++++++ yazi-fs/src/trash/mod.rs | 16 + yazi-fs/src/trash/node.rs | 49 +++ yazi-fs/src/trash/nodes.rs | 36 ++ yazi-fs/src/trash/traits.rs | 36 ++ yazi-fs/src/trash/unsupported.rs | 46 +++ yazi-fs/src/trash/windows.rs | 330 ++++++++++++++++++ yazi-fs/src/url.rs | 85 +++-- yazi-macro/src/event.rs | 6 +- yazi-macro/src/render.rs | 2 +- yazi-plugin/preset/plugins/archive.lua | 2 +- yazi-plugin/preset/plugins/image.lua | 6 +- yazi-plugin/preset/plugins/mime-local.lua | 10 +- yazi-plugin/preset/plugins/mime-remote.lua | 8 +- yazi-plugin/preset/plugins/trash.lua | 153 +++++++++ yazi-plugin/preset/setup.lua | 1 + yazi-plugin/src/external/fd.rs | 6 +- yazi-plugin/src/external/rg.rs | 6 +- yazi-plugin/src/external/rga.rs | 6 +- yazi-plugin/src/fs/calculator.rs | 6 +- yazi-plugin/src/fs/fs.rs | 25 +- yazi-plugin/src/fs/op.rs | 5 +- yazi-plugin/src/utils/cache.rs | 27 +- yazi-plugin/src/utils/image.rs | 24 +- yazi-plugin/src/utils/preview.rs | 7 +- yazi-runner/src/loader/loader.rs | 1 + yazi-runner/src/provider/job.rs | 14 +- yazi-scheduler/src/fetch/fetch.rs | 4 +- yazi-scheduler/src/file/file.rs | 23 +- yazi-scheduler/src/file/traverse.rs | 4 +- yazi-scheduler/src/preload/preload.rs | 4 +- yazi-scheduler/src/scheduler.rs | 4 +- yazi-sftp/Cargo.toml | 1 + yazi-sftp/src/fs/attrs.rs | 3 +- yazi-sftp/src/requests/init.rs | 3 +- yazi-sftp/src/responses/version.rs | 3 +- yazi-sftp/src/session.rs | 3 +- yazi-shared/src/auth/auth.rs | 9 - yazi-shared/src/auth/de.rs | 109 ++++++ yazi-shared/src/auth/domain.rs | 114 +++--- yazi-shared/src/auth/encode.rs | 32 +- yazi-shared/src/auth/mod.rs | 2 +- yazi-shared/src/data/de_bytes.rs | 2 +- yazi-shared/src/path/error.rs | 4 + yazi-shared/src/path/lua.rs | 5 +- yazi-shared/src/path/path.rs | 5 + yazi-shared/src/spec/spec.rs | 6 +- yazi-shared/src/strand/error.rs | 4 + yazi-shared/src/strand/strand.rs | 11 +- yazi-shared/src/tests.rs | 6 +- yazi-shared/src/url/de.rs | 10 +- yazi-shared/src/url/lua.rs | 8 +- yazi-shared/src/url/traits.rs | 21 +- yazi-shared/src/url/url.rs | 2 +- yazi-shim/Cargo.toml | 3 + yazi-shim/src/lib.rs | 2 +- yazi-shim/src/option.rs | 7 + yazi-vfs/src/cha.rs | 14 +- yazi-vfs/src/engine/demand.rs | 6 +- yazi-vfs/src/engine/dir_entry.rs | 10 +- yazi-vfs/src/engine/engine.rs | 13 +- yazi-vfs/src/engine/engines.rs | 33 +- yazi-vfs/src/engine/lua/file.rs | 10 + yazi-vfs/src/engine/lua/lua.rs | 12 +- yazi-vfs/src/engine/lua/read_dir.rs | 2 + yazi-vfs/src/engine/rw_file.rs | 105 ++++-- yazi-vfs/src/engine/sftp/read_dir.rs | 8 +- yazi-vfs/src/entries.rs | 37 +- yazi-vfs/src/file.rs | 22 +- yazi-vfs/src/fns.rs | 3 +- yazi-vfs/src/lib.rs | 2 +- yazi-vfs/src/op.rs | 21 -- yazi-vfs/src/stamp.rs | 108 ++++++ yazi-watcher/Cargo.toml | 2 +- yazi-watcher/src/backend.rs | 14 +- yazi-watcher/src/lib.rs | 4 +- yazi-watcher/src/local/local.rs | 28 +- yazi-watcher/src/refresher.rs | 183 ++++++++++ yazi-watcher/src/remote/mod.rs | 1 - yazi-watcher/src/remote/remote.rs | 61 ---- yazi-watcher/src/reporter.rs | 27 +- yazi-watcher/src/virtual/mod.rs | 1 + yazi-watcher/src/virtual/virtual.rs | 85 +++++ yazi-watcher/src/watched.rs | 79 +++-- yazi-watcher/src/watchee.rs | 12 +- yazi-watcher/src/watcher.rs | 45 +-- 151 files changed, 3053 insertions(+), 1089 deletions(-) create mode 100644 yazi-ffi/src/com.rs create mode 100644 yazi-fs/src/auth.rs create mode 100644 yazi-fs/src/cha/cha_sig.rs create mode 100644 yazi-fs/src/file/file_extra.rs create mode 100644 yazi-fs/src/file/file_sig.rs delete mode 100644 yazi-fs/src/path/percent.rs delete mode 100644 yazi-fs/src/spec.rs create mode 100644 yazi-fs/src/trash/entry.rs create mode 100644 yazi-fs/src/trash/freedesktop.rs create mode 100644 yazi-fs/src/trash/lua.rs create mode 100644 yazi-fs/src/trash/macos.rs create mode 100644 yazi-fs/src/trash/mod.rs create mode 100644 yazi-fs/src/trash/node.rs create mode 100644 yazi-fs/src/trash/nodes.rs create mode 100644 yazi-fs/src/trash/traits.rs create mode 100644 yazi-fs/src/trash/unsupported.rs create mode 100644 yazi-fs/src/trash/windows.rs create mode 100644 yazi-plugin/preset/plugins/trash.lua create mode 100644 yazi-shared/src/auth/de.rs create mode 100644 yazi-shim/src/option.rs delete mode 100644 yazi-vfs/src/op.rs create mode 100644 yazi-vfs/src/stamp.rs create mode 100644 yazi-watcher/src/refresher.rs delete mode 100644 yazi-watcher/src/remote/mod.rs delete mode 100644 yazi-watcher/src/remote/remote.rs create mode 100644 yazi-watcher/src/virtual/mod.rs create mode 100644 yazi-watcher/src/virtual/virtual.rs diff --git a/.github/workflows/validate-issue.yml b/.github/workflows/validate-issue.yml index 7e997036..2fb7bc4b 100644 --- a/.github/workflows/validate-issue.yml +++ b/.github/workflows/validate-issue.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 20 diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 17d9009e..a4640a92 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -1,7 +1,7 @@ name: Validate PR on: - pull_request_target: + pull_request: types: [opened, edited, reopened, synchronize] permissions: @@ -17,7 +17,7 @@ jobs: ref: ${{ github.event.repository.default_branch }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 20 diff --git a/CHANGELOG.md b/CHANGELOG.md index 09209fa6..0c6b4322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Added - Drag and drop ([#4005]) +- Trash bin ([#4144]) - Bulk create ([#3793]) - Make help menu a command palette ([#4074]) - Input history ([#4104]) @@ -1781,3 +1782,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4108]: https://github.com/sxyazi/yazi/pull/4108 [#4118]: https://github.com/sxyazi/yazi/pull/4118 [#4120]: https://github.com/sxyazi/yazi/pull/4120 +[#4144]: https://github.com/sxyazi/yazi/pull/4144 diff --git a/Cargo.lock b/Cargo.lock index cd76337b..8b243762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,7 +106,7 @@ dependencies = [ "ratatui-core", "simdutf8", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -198,7 +198,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -213,6 +213,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayvec" version = "0.7.8" @@ -258,7 +264,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "y4m", ] @@ -349,6 +355,30 @@ dependencies = [ "serde", ] +[[package]] +name = "binrw" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53195f985e88ab94d1cc87e80049dd2929fd39e4a772c5ae96a7e5c4aad3642" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5910da05ee556b789032c8ff5a61fb99239580aa3fd0bfaa8f4d094b2aee00ad" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -357,9 +387,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -445,7 +475,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.119", ] [[package]] @@ -459,9 +489,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "serde_core", @@ -487,9 +517,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] @@ -502,7 +532,7 @@ checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -543,9 +573,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -561,9 +591,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -606,9 +636,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -616,9 +646,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -647,9 +677,9 @@ dependencies = [ [[package]] name = "clap_complete_nushell" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897" +checksum = "933b05d5d83ff65fd7eaf5d106c792f2264908790a2642aca57429767b762ce2" dependencies = [ "clap", "clap_complete", @@ -664,7 +694,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -715,6 +745,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_str" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "serde", + "static_assertions", + "zmij", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -899,7 +943,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -922,7 +966,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -933,7 +977,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -970,7 +1014,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1035,6 +1079,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ds_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d632a0239fbe380ec79e8f80f5d2643da25ecfd61cde10d8c9e927d1c035acb" +dependencies = [ + "binrw", + "thiserror 2.0.19", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1125,7 +1179,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1145,7 +1199,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1216,9 +1270,9 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fax" @@ -1306,9 +1360,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1321,9 +1375,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1331,15 +1385,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1348,38 +1402,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1477,9 +1531,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1735,7 +1789,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1758,7 +1812,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1820,7 +1874,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2024,9 +2078,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -2092,7 +2146,7 @@ checksum = "0232231813b214d44b57c7678e7dbbcfaac35bd1a01acef0dad7c5dfdc1b1973" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2228,7 +2282,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2374,6 +2428,12 @@ dependencies = [ "serde", ] +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + [[package]] name = "p256" version = "0.14.0-rc.15" @@ -2429,7 +2489,7 @@ dependencies = [ "log", "rand 0.10.2", "sha2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "windows 0.62.2", "windows-strings", @@ -2457,7 +2517,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2635,9 +2695,9 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b20f20e954175de5f463f67781b35583397d916b1d148738923711b2ad16bee8" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", "cpufeatures", @@ -2646,9 +2706,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "powerfmt" @@ -2672,7 +2732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] @@ -2704,9 +2764,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -2727,7 +2787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2801,9 +2861,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2898,7 +2958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ "bitflags", - "compact_str", + "compact_str 0.9.1", "critical-section", "hashbrown 0.17.1", "itertools", @@ -2907,7 +2967,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -2963,7 +3023,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha", "simd_helpers", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "wasm-bindgen", ] @@ -3034,34 +3094,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3071,9 +3131,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3199,7 +3259,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "typenum", "universal-hash", @@ -3278,9 +3338,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safe_arch" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed" +checksum = "3a52ec151f024d703f9fd65abb7cbe81e7cdb39f18917a3a37e3014470dc7c59" dependencies = [ "bytemuck", ] @@ -3368,9 +3428,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3388,29 +3448,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3457,7 +3517,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3561,9 +3621,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_helpers" @@ -3594,9 +3654,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3707,7 +3767,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3724,9 +3784,20 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -3749,7 +3820,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", ] @@ -3783,11 +3854,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -3798,18 +3869,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -3857,9 +3928,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "libc", @@ -3879,9 +3950,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3904,9 +3975,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -3921,13 +3992,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3956,9 +4027,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -3989,9 +4060,9 @@ dependencies = [ [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -4012,7 +4083,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tracing-subscriber", ] @@ -4025,7 +4096,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4087,11 +4158,11 @@ dependencies = [ [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" dependencies = [ - "rand 0.9.5", + "rand 0.10.2", ] [[package]] @@ -4319,7 +4390,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -4340,9 +4411,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" -version = "8.0.4" +version = "8.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" dependencies = [ "libc", ] @@ -4441,7 +4512,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4452,7 +4523,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4463,7 +4534,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4474,7 +4545,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4696,9 +4767,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -4873,6 +4944,7 @@ dependencies = [ "yazi-fs", "yazi-macro", "yazi-shared", + "yazi-shim", "yazi-term", "yazi-tty", "yazi-version", @@ -4884,7 +4956,7 @@ version = "26.5.6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -4904,6 +4976,7 @@ dependencies = [ "regex", "serde", "serde_with", + "strum", "toml", "tracing", "yazi-binding", @@ -5007,6 +5080,7 @@ dependencies = [ "core-foundation-sys", "libc", "objc2", + "windows 0.62.2", "yazi-macro", ] @@ -5061,7 +5135,9 @@ dependencies = [ "arc-swap", "bitflags", "core-foundation-sys", + "data-encoding", "dirs", + "ds_parser", "either", "foldhash", "hashbrown 0.17.1", @@ -5070,17 +5146,18 @@ dependencies = [ "mlua", "objc2", "parking_lot", - "percent-encoding", "rand 0.10.2", "regex", "scopeguard", "serde", + "serde_with", "strum", "tokio", "tracing", "trash", "typed-path", "uzers", + "windows 0.62.2", "windows-sys 0.61.2", "yazi-binding", "yazi-ffi", @@ -5198,7 +5275,7 @@ dependencies = [ "mlua", "parking_lot", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tracing", @@ -5245,6 +5322,7 @@ name = "yazi-sftp" version = "26.5.6" dependencies = [ "bitflags", + "hashbrown 0.17.1", "parking_lot", "russh", "serde", @@ -5273,7 +5351,7 @@ dependencies = [ "serde", "serde_with", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "typed-path", "uzers", @@ -5297,8 +5375,9 @@ dependencies = [ "ratatui-core", "ratatui-widgets", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml", + "trash", "twox-hash", "unicode-segmentation", "unicode-width", @@ -5312,7 +5391,7 @@ dependencies = [ "anyhow", "base64", "bitflags", - "compact_str", + "compact_str 0.10.0", "futures", "mlua", "parking_lot", @@ -5320,7 +5399,7 @@ dependencies = [ "serde", "signal-hook", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "windows-sys 0.61.2", "yazi-macro", @@ -5401,9 +5480,9 @@ version = "26.5.6" dependencies = [ "futures", "hashbrown 0.17.1", + "indexmap 2.14.0", "notify", "parking_lot", - "percent-encoding", "tokio", "tokio-stream", "tracing", @@ -5460,7 +5539,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5471,9 +5550,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index a4f5aa33..af8f8145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,20 +36,21 @@ debug = false [workspace.dependencies] ansi-to-tui = "8.0.1" -anyhow = "1.0.103" +anyhow = "1.0.104" arc-swap = { version = "1.9.2", features = [ "serde" ] } base64 = "0.22.1" -bitflags = { version = "2.13.0", features = [ "serde" ] } +bitflags = { version = "2.13.1", features = [ "serde" ] } chrono = "0.4.45" -clap = { version = "4.6.1", features = [ "derive" ] } -compact_str = { version = "0.9.1", features = [ "serde" ] } +clap = { version = "4.6.2", features = [ "derive" ] } +compact_str = { version = "0.10.0", features = [ "serde" ] } core-foundation-sys = "0.8.7" +data-encoding = "2.11.0" dirs = "6.0.0" dyn-clone = "1.0.20" either = { version = "1.16.0" } foldhash = "0.2.0" -futures = "0.3.32" -globset = "0.4.18" +futures = "0.3.33" +globset = "0.4.19" hashbrown = { version = "0.17.1", features = [ "serde" ] } image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] } indexmap = { version = "2.14.0", features = [ "serde" ] } @@ -65,21 +66,21 @@ percent-encoding = "2.3.2" rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] } ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] } ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] } -regex = "1.13.0" +regex = "1.13.1" russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" -serde = { version = "1.0.228", features = [ "derive" ] } -serde_json = "1.0.150" +serde = { version = "1.0.229", features = [ "derive" ] } +serde_json = "1.0.151" serde_with = "3.21.0" strum = { version = "0.28.0", features = [ "derive" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } -thiserror = "2.0.18" -tokio = { version = "1.52.3", features = [ "full" ] } +thiserror = "2.0.19" +tokio = { version = "1.53.0", features = [ "full" ] } tokio-stream = "0.1.18" tokio-util = "0.7.18" -toml = { version = "1.1.2" } +toml = { version = "1.1.3" } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } -twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } +twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] } typed-path = "0.12.3" unicode-width = { version = "0.2.2", default-features = false } uzers = "0.12.2" diff --git a/yazi-actor/src/mgr/bulk_create.rs b/yazi-actor/src/mgr/bulk_create.rs index ea054392..8179074c 100644 --- a/yazi-actor/src/mgr/bulk_create.rs +++ b/yazi-actor/src/mgr/bulk_create.rs @@ -4,16 +4,16 @@ use anyhow::{Result, anyhow}; use scopeguard::defer; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRuleArc}; -use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}, file::File}; +use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}}; use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; -use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shared::{data::Data, strand::Strand, url::{UrlBuf, UrlLike}}; use yazi_shim::path::CROSS_SEPARATOR; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; -use yazi_vfs::{VfsFile, engine}; +use yazi_vfs::engine; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -31,7 +31,7 @@ impl Actor for BulkCreate { let cwd = cx.cwd().clone(); tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk-create"); - engine::create_new(&tmp).await?; + let file = engine::create_new(&tmp).await?.file().await?; defer! { let tmp = tmp.clone(); @@ -42,7 +42,7 @@ impl Actor for BulkCreate { TasksProxy::process_exec(ShellOpt { cwd: cwd.clone(), - cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run), + cmd: Splatter::new(&[file]).splat(&opener.run), block: opener.block, orphan: opener.orphan, }) @@ -86,7 +86,7 @@ impl BulkCreate { if let Err(e) = result { failed.push((entry, e.into())); - } else if let Ok(f) = File::new(dist).await { + } else if let Ok(f) = engine::file(dist).await { succeeded.push(f); } else { failed.push((entry, anyhow!("Failed to retrieve file info"))); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 9e2865f5..19dbeb69 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -7,7 +7,7 @@ use tokio::io::AsyncWriteExt; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRuleArc}; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::{Demand, Local}}, file::File, max_common_root, path::skip_url}; +use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url}; use yazi_macro::{err, succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; @@ -15,7 +15,7 @@ use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; -use yazi_vfs::{VfsFile, engine, maybe_exists}; +use yazi_vfs::{engine::{self, Demand}, maybe_exists}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -46,13 +46,8 @@ impl Actor for BulkRename { tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk-rename"); - Demand::default() - .write(true) - .create_new(true) - .open(&tmp) - .await? - .write_all(old.join(Strand::Utf8("\n")).encoded_bytes()) - .await?; + let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?; + rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?; defer! { let tmp = tmp.clone(); @@ -65,7 +60,7 @@ impl Actor for BulkRename { batcher.prime(&tmp); TasksProxy::process_exec(ShellOpt { cwd, - cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run), + cmd: Splatter::new(&[rw.into_file().await?]).splat(&opener.run), block: opener.block, orphan: opener.orphan, }) @@ -131,7 +126,7 @@ impl BulkRename { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = engine::rename(&old, &new).await { failed.push((o, n, e.into())); - } else if let Ok(f) = File::new(new).await { + } else if let Ok(f) = engine::file(new).await { succeeded.insert(old, f); } else { failed.push((o, n, anyhow!("Failed to retrieve file info"))); diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 2e6b9efc..e0c6cae0 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -6,12 +6,12 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::YAZI; use yazi_core::mgr::CdSource; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, file::File, path::{clean_url, expand_url}}; +use yazi_fs::{FilesOp, path::{clean_url, expand_url}}; use yazi_macro::{act, err, input, render, succ}; use yazi_parser::mgr::CdForm; use yazi_proxy::{CmpProxy, MgrProxy}; use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, engine}; +use yazi_vfs::engine; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; @@ -76,7 +76,7 @@ impl Cd { let Ok(url) = engine::absolute(&url).await else { return }; let url = clean_url(url); - let Ok(file) = File::new(&url).await else { return }; + let Ok(file) = engine::file(&url).await else { return }; if file.is_dir() { return MgrProxy::cd(&url, CdSource::Cd); } diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 3bd14da7..9c3bde82 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -80,7 +80,7 @@ impl Create { if let Ok(real) = engine::casefold(&new).await && let Some((parent, key)) = real.pair2() { - let file = File::new(&real).await?; + let file = engine::file(&real).await?; FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit(); MgrProxy::reveal(&real); } diff --git a/yazi-actor/src/mgr/download.rs b/yazi-actor/src/mgr/download.rs index 1a6da9ee..34cc44b0 100644 --- a/yazi-actor/src/mgr/download.rs +++ b/yazi-actor/src/mgr/download.rs @@ -4,12 +4,12 @@ use anyhow::Result; use futures::{StreamExt, stream::FuturesUnordered}; use hashbrown::HashSet; use yazi_core::mgr::OpenOpt; -use yazi_fs::{FsSpec, engine::{Engine, local::Local}, file::File}; +use yazi_fs::{FsAuth, FsUrl, engine::{Engine, local::Local}}; use yazi_macro::succ; use yazi_parser::mgr::DownloadForm; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; use crate::{Actor, Ctx}; @@ -42,7 +42,7 @@ impl Actor for Download { continue; } - let Ok(f) = File::new(&url).await else { continue }; + let Ok(f) = engine::file(&url).await else { continue }; urls.push(url); files.push(f); @@ -74,10 +74,12 @@ impl Actor for Download { impl Download { async fn prepare(urls: &[UrlBuf]) { - let roots: HashSet<_> = urls.iter().filter_map(|u| u.auth().cache()).collect(); - for mut root in roots { - root.push("%lock"); - Local::regular(&root).create_dir_all().await.ok(); + let stamp_roots = urls.iter().filter_map(|u| u.auth().stamp_root()); + let bucket_dirs = urls.iter().filter_map(|u| u.parent()?.cache_bucket()); + + let dirs: HashSet<_> = stamp_roots.chain(bucket_dirs).collect(); + for dir in dirs { + Local::regular(&dir).create_dir_all().await.ok(); } } } diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index 50d9e113..a51f53eb 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -16,7 +16,7 @@ impl Actor for Follow { fn act(cx: &mut Ctx, _: Self::Form) -> Result { let Some(file) = cx.hovered() else { succ!() }; - let Some(link_to) = &file.link_to else { succ!() }; + let Some(link_to) = file.extra.link_to() else { succ!() }; let Some(parent) = file.url.parent() else { succ!() }; let Ok(joined) = parent.try_join(link_to) else { succ!() }; act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow)) diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 02cb0ad5..75f7ac63 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -4,6 +4,7 @@ use yazi_fs::FolderStage; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::{mgr::HiddenForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; +use yazi_shim::OptionExt; use crate::{Actor, Ctx}; @@ -18,7 +19,7 @@ impl Actor for Hidden { let state = form.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; - let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key()).owned(); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -30,9 +31,7 @@ impl Actor for Hidden { }; // Apply to CWD and parent - if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) - && (a | b) - { + if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; } diff --git a/yazi-actor/src/mgr/open.rs b/yazi-actor/src/mgr/open.rs index 492abb61..03bef4a5 100644 --- a/yazi-actor/src/mgr/open.rs +++ b/yazi-actor/src/mgr/open.rs @@ -3,12 +3,11 @@ use futures::StreamExt; use hashbrown::HashSet; use yazi_boot::ARGS; use yazi_core::mgr::OpenDoOpt; -use yazi_fs::file::File; use yazi_macro::{act, succ}; use yazi_parser::mgr::OpenForm; use yazi_proxy::MgrProxy; use yazi_shared::data::Data; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; use crate::{Actor, Ctx, mgr::Quit}; @@ -59,7 +58,7 @@ impl Actor for Open { let it = futures::stream::iter(opt.targets) .enumerate() - .map(|(i, url)| async move { File::new(url).await.ok().map(|file| (i, file)) }) + .map(|(i, url)| async move { engine::file(url).await.ok().map(|file| (i, file)) }) .buffered(3) .filter_map(|item| async move { item }); diff --git a/yazi-actor/src/mgr/open_do.rs b/yazi-actor/src/mgr/open_do.rs index db6232bd..86ab8f6d 100644 --- a/yazi-actor/src/mgr/open_do.rs +++ b/yazi-actor/src/mgr/open_do.rs @@ -42,10 +42,10 @@ impl Actor for OpenDo { } let pick = PickProxy::show(YAZI.pick.open(openers.iter().map(|o| o.desc()).collect())); - let urls: Vec<_> = targets.into_iter().map(|(file, _)| file.url).collect(); + let files: Vec<_> = targets.into_iter().map(|(file, _)| file).collect(); tokio::spawn(async move { if let Some(choice) = pick.await { - Self::open_with(&openers[choice], &opt.cwd, &urls); + Self::open_with(&openers[choice], &opt.cwd, &files); } }); succ!(); @@ -59,20 +59,20 @@ impl OpenDo { if let Some(open) = YAZI.open.matches(&file, mime) && let Some(opener) = YAZI.opener.first(&open) { - openers.entry(opener).or_default().push(file.url); + openers.entry(opener).or_default().push(file); } } - for (opener, urls) in openers { - Self::open_with(&opener, &cwd, &urls); + for (opener, files) in openers { + Self::open_with(&opener, &cwd, &files); } } - fn open_with(opener: &OpenerRule, cwd: &UrlBuf, urls: &[UrlBuf]) { - let size = if opener.spread { urls.len().max(1) } else { 1 }; - for urls in urls.chunks(size) { + fn open_with(opener: &OpenerRule, cwd: &UrlBuf, files: &[File]) { + let size = if opener.spread { files.len().max(1) } else { 1 }; + for files in files.chunks(size) { TasksProxy::process_open(ShellOpt { cwd: cwd.clone(), - cmd: Splatter::new(urls).splat(&opener.run), + cmd: Splatter::new(files).splat(&opener.run), block: opener.block, orphan: opener.orphan, }); diff --git a/yazi-actor/src/mgr/peek.rs b/yazi-actor/src/mgr/peek.rs index c1d41d28..e9a00e88 100644 --- a/yazi-actor/src/mgr/peek.rs +++ b/yazi-actor/src/mgr/peek.rs @@ -21,18 +21,14 @@ impl Actor for Peek { } let mime = cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default(); - let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha)); + let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.file.clone())); if !cx.tab().preview.same_url(&hovered.url) { - cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default(); + cx.tab_mut().preview.skip = folder.as_ref().map(|f| f.0).unwrap_or_default(); } if !cx.tab().preview.same_file(&hovered, &mime) { cx.tab_mut().preview.reset(); } - if !cx.tab().preview.same_folder(&hovered.url) { - cx.tab_mut().preview.folder_lock = None; - } - if matches!(form.only_if, Some(u) if u != hovered.url) { succ!(); } @@ -46,11 +42,13 @@ impl Actor for Peek { } } - if hovered.is_dir() { - cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), mime, form.force); - } else { - cx.tab_mut().preview.go(hovered, mime, form.force); + if let Some((_, file)) = folder { + cx.core.mgr.watcher.refresher.refresh([file]); + } else if hovered.is_dir() { + cx.core.mgr.watcher.refresher.load(&hovered); } + + cx.tab_mut().preview.go(hovered, mime, form.force); succ!(); } } diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index abbf1888..2ea4598d 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -1,10 +1,8 @@ use anyhow::Result; -use yazi_core::tab::Folder; -use yazi_fs::{CWD, Entries, FilesOp, cha::Cha}; +use yazi_fs::CWD; use yazi_macro::{act, succ}; use yazi_parser::VoidForm; -use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsEntries, VfsFilesOp}; +use yazi_shared::{data::Data, url::UrlLike}; use yazi_watcher::MgrProxy; use crate::{Actor, Ctx}; @@ -19,11 +17,13 @@ impl Actor for Refresh { fn act(cx: &mut Ctx, _: Self::Form) -> Result { CWD.set(cx.cwd(), Self::cwd_changed); - if let Some(p) = cx.parent() { - Self::trigger_dirs(&[cx.current(), p]); - } else { - Self::trigger_dirs(&[cx.current()]); - } + cx.core.mgr.watcher.refresher.refresh( + [Some(cx.current()), cx.parent()] + .into_iter() + .flatten() + .filter(|f| f.url.is_absolute() && !f.url.is_search()) + .map(|f| &f.file), + ); act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; @@ -40,26 +40,4 @@ impl Refresh { MgrProxy::watch(); } } - - // TODO: performance improvement - fn trigger_dirs(folders: &[&Folder]) { - async fn go(dir: UrlBuf, cha: Cha) { - let Some(cha) = Entries::assert_stale(&dir, cha).await else { return }; - - match Entries::from_dir_bulk(&dir).await { - Ok(files) => FilesOp::Full(dir, files, cha).emit(), - Err(e) => FilesOp::issue_error(&dir, e).await, - } - } - - let futs: Vec<_> = folders - .iter() - .filter(|&f| f.url.is_absolute() && !f.url.is_search()) - .map(|&f| go(f.url.clone(), f.cha)) - .collect(); - - if !futs.is_empty() { - tokio::spawn(futures::future::join_all(futs)); - } - } } diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 031134fe..c3752754 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -84,7 +84,7 @@ impl Rename { let new = engine::casefold(&new).await?; let Some((new_p, new_k)) = new.pair2() else { return Ok(()) }; - let file = File::new(&new).await?; + let file = engine::file(&new).await?; if new_p == old_p { FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit(); } else { diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index 70b380a8..87ba3be6 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::YAZI; use yazi_core::mgr::{CdSource, SearchVia}; -use yazi_fs::{FilesOp, cha::Cha}; +use yazi_fs::{FilesOp, cha::ChaType, file::File}; use yazi_macro::{act, input, succ}; use yazi_parser::{VoidForm, mgr::SearchForm}; use yazi_plugin::external; @@ -89,7 +89,7 @@ impl Actor for SearchDo { while let Some(chunk) = rx.next().await { FilesOp::Part(cwd.clone(), chunk, ticket).emit(); } - FilesOp::Done(cwd, Cha::default(), ticket).emit(); + FilesOp::Done(File::from_dummy(cwd, Some(ChaType::Dir)), ticket).emit(); Ok(()) })); diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index dd94b4f8..a3af3785 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -4,6 +4,7 @@ use yazi_fs::{FilesSorter, FolderStage}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::{mgr::SortForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; +use yazi_shim::OptionExt; use crate::{Actor, Ctx}; @@ -24,7 +25,7 @@ impl Actor for Sort { pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback); let sorter = FilesSorter::from(&*pref); - let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key()).owned(); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -36,9 +37,7 @@ impl Actor for Sort { }; // Apply to CWD and parent - if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) - && (a | b) - { + if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.entries); diff --git a/yazi-actor/src/mgr/watch.rs b/yazi-actor/src/mgr/watch.rs index d1dcea18..92e70fde 100644 --- a/yazi-actor/src/mgr/watch.rs +++ b/yazi-actor/src/mgr/watch.rs @@ -15,9 +15,11 @@ impl Actor for Watch { const NAME: &str = "watch"; fn act(cx: &mut Ctx, _: Self::Form) -> Result { - let it = iter::once(cx.core.mgr.tabs.active().cwd()) - .chain(cx.core.mgr.tabs.parent().map(|p| &p.url)) - .chain(cx.core.mgr.tabs.hovered().filter(|h| h.is_dir()).map(|h| &h.url)); + let tab = cx.core.mgr.tabs.active(); + + let it = iter::once(&tab.current.file) + .chain(tab.hovered_folder().map(|h| &h.file).or(tab.hovered().filter(|f| f.is_dir()))) + .chain(tab.parent.as_ref().map(|p| &p.file)); cx.core.mgr.watcher.watch(it); succ!(); diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index b2fd3ac1..ed68c9df 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -209,20 +209,18 @@ macro_rules! impl_file_fields { $fields.add_cached_field("cha", |_, me| Ok(me.cha)); $fields.add_cached_field("url", |_, me| Ok(me.url_owned())); - $fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone())); + $fields.add_cached_field("link_to", |_, me| Ok(me.extra.link_to().cloned())); $fields.add_cached_field("name", |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); $fields.add_cached_field("path", |_, me| { - use yazi_fs::FsUrl; - use yazi_shared::{path::PathBufDyn, url::AsUrl}; - Ok(PathBufDyn::from(me.url.as_url().unified_path())) + use yazi_shared::path::PathBufDyn; + Ok(PathBufDyn::from(me.content_path())) }); $fields.add_cached_field("cache", |_, me| { - use yazi_fs::FsUrl; use yazi_shared::path::PathBufDyn; - Ok(me.url.cache().map(PathBufDyn::from)) + Ok(me.cache().map(PathBufDyn::from)) }); }; } @@ -231,8 +229,8 @@ macro_rules! impl_file_fields { macro_rules! impl_file_methods { ($methods:ident) => { $methods.add_method("hash", |_, me, ()| { - use yazi_fs::FsHash64; - Ok(me.hash_u64()) + use yazi_fs::{FsHash64, file::FileSig}; + Ok(FileSig(me).hash_u64()) }); }; } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index e4fc91fd..b5b1dff7 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -32,4 +32,4 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } clap = { workspace = true } clap_complete = "4.6.7" clap_complete_fig = "4.5.2" -clap_complete_nushell = "4.6.0" +clap_complete_nushell = "4.6.1" diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 67fb422a..508d92f5 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -21,6 +21,7 @@ yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } +yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.6" } yazi-version = { path = "../yazi-version", version = "26.5.6" } @@ -44,7 +45,7 @@ anyhow = { workspace = true } clap = { workspace = true } clap_complete = "4.6.7" clap_complete_fig = "4.5.2" -clap_complete_nushell = "4.6.0" +clap_complete_nushell = "4.6.1" serde = { workspace = true } serde_json = { workspace = true } diff --git a/yazi-cli/src/env/env.rs b/yazi-cli/src/env/env.rs index 1cc8e981..7e353d2d 100644 --- a/yazi-cli/src/env/env.rs +++ b/yazi-cli/src/env/env.rs @@ -5,6 +5,7 @@ use yazi_config::{THEME, YAZI}; use yazi_emulator::Mux; use yazi_fs::Xdg; use yazi_shared::timestamp_us; +use yazi_shim::OptionExt; use yazi_term::TERM; use crate::env::Env; @@ -156,7 +157,8 @@ impl Env { Regex::new(r"\d+\.\d+(\.\d+-\d+|\.\d+|\b)") .unwrap() .find(&line) - .map(|m| m.as_str().to_owned()) + .map(|m| m.as_str()) + .owned() .unwrap_or(line) } Ok(out) => format!("{:?}, {:?}", out.status, String::from_utf8_lossy(&out.stderr)), diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 4dfcf0dc..830012a2 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -18,5 +18,5 @@ proc-macro = true [dependencies] # External dependencies proc-macro2 = "1" -quote = "1.0.46" -syn = { version = "2.0.118", features = [ "full" ] } +quote = "1.0.47" +syn = { version = "3.0.2", features = [ "full" ] } diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index d50defa4..719b8da6 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -37,5 +37,6 @@ ratatui-widgets = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } +strum = { workspace = true } toml = { workspace = true } tracing = { workspace = true } diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index db1fa4ff..9e4c4421 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -128,8 +128,9 @@ keymap = [ # Goto { on = [ "g", "h" ], run = "cd ~", desc = "Go home" }, - { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" }, - { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" }, + { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go to ~/.config" }, + { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go to ~/Downloads" }, + { on = [ "g", "t" ], run = "plugin trash", desc = "Go to trash bin" }, { on = [ "g", "" ], run = "cd --interactive", desc = "Jump interactively" }, { on = [ "g", "f" ], run = "follow", desc = "Follow hovered symlink" }, diff --git a/yazi-config/preset/vfs-default.toml b/yazi-config/preset/vfs-default.toml index e69de29b..b050b697 100644 --- a/yazi-config/preset/vfs-default.toml +++ b/yazi-config/preset/vfs-default.toml @@ -0,0 +1,3 @@ +[trash."*"] +kind = "hub" +run = "trash" diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 1fc65c7f..0f82e7b0 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -63,9 +63,16 @@ download = [ { run = "ya emit download --open %S", desc = "Download and open" }, { run = "ya emit download %S", desc = "Download" }, ] +trash = [ + { run = "ya pub trash-restore --list %S", desc = "Restore selected files" }, + { run = "ya pub trash-empty --list %S", desc = "Empty trash bin" }, +] [open] rules = [ + # Trash + { url = "trash://*", use = [ "open", "trash" ] }, + { url = "trash://*/", use = [ "edit", "trash" ] }, # Folder { url = "*/", use = [ "edit", "open", "reveal" ] }, # Text @@ -102,6 +109,7 @@ fetchers = [ # MIME-type { url = "*/", run = "mime.dir", prio = "high", group = "mime" }, { url = "local://*", run = "mime.local", prio = "high", group = "mime" }, + { url = "trash://*", run = "mime.local", prio = "high", group = "mime" }, { url = "remote://*", run = "mime.remote", prio = "high", group = "mime" }, ] spotters = [ diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index a632b0a8..e5afe31c 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -2,13 +2,13 @@ use std::{fmt::Debug, str::FromStr}; use anyhow::{Result, bail}; use globset::{Candidate, GlobBuilder}; -use serde::Deserialize; +use serde_with::DeserializeFromStr; +use strum::EnumIs; use yazi_shared::{auth::Auth, url::AsUrl}; use crate::Mixable; -#[derive(Clone, Deserialize)] -#[serde(try_from = "String")] +#[derive(Clone, DeserializeFromStr)] pub struct Pattern { inner: globset::GlobMatcher, scheme: PatternScheme, @@ -98,21 +98,14 @@ impl FromStr for Pattern { } } -// FIXME: remove -impl TryFrom for Pattern { - type Error = anyhow::Error; - - fn try_from(s: String) -> Result { Self::from_str(s.as_str()) } -} - impl Mixable for Pattern { - fn any_file(&self) -> bool { self.is_star && !self.is_dir } + fn any_file(&self) -> bool { self.is_star && !self.is_dir && self.scheme.is_any() } - fn any_dir(&self) -> bool { self.is_star && self.is_dir } + fn any_dir(&self) -> bool { self.is_star && self.is_dir && self.scheme.is_any() } } // --- Scheme -#[derive(Clone, Debug)] +#[derive(Clone, Debug, EnumIs)] enum PatternScheme { Any, Local, diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index fecb4de4..8019aeea 100644 --- a/yazi-core/src/mgr/snap.rs +++ b/yazi-core/src/mgr/snap.rs @@ -1,12 +1,11 @@ -use yazi_fs::Splatable; -use yazi_shared::url::{AsUrl, Url, UrlBuf}; +use yazi_fs::{Splatable, file::File}; use crate::{mgr::Mgr, tab::TabSnap}; pub struct MgrSnap { tab: usize, tabs: Vec, - yanked: Vec, + yanked: Vec, } impl From<&Mgr> for MgrSnap { @@ -14,7 +13,7 @@ impl From<&Mgr> for MgrSnap { Self { tab: value.tabs.cursor, tabs: value.tabs.iter().map(Into::into).collect(), - yanked: value.yanked.urls().cloned().collect(), + yanked: value.yanked.files().cloned().collect(), } } } @@ -22,7 +21,7 @@ impl From<&Mgr> for MgrSnap { impl Splatable for MgrSnap { fn tab(&self) -> usize { self.tab + 1 } - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); tab .checked_sub(1) @@ -31,24 +30,14 @@ impl Splatable for MgrSnap { .iter() .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) - .map(AsUrl::as_url) } - fn hovered(&self, tab: usize) -> Option> { - tab - .checked_sub(1) - .and_then(|tab| self.tabs.get(tab)) - .and_then(|tab| tab.hovered.as_ref()) - .map(AsUrl::as_url) + fn hovered(&self, tab: usize) -> Option<&File> { + tab.checked_sub(1).and_then(|tab| self.tabs.get(tab)).and_then(|tab| tab.hovered.as_ref()) } - fn yanked(&self, mut idx: Option) -> impl Iterator> { + fn yanked(&self, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); - self - .yanked - .iter() - .skip(idx.unwrap_or(0)) - .take(if idx.is_some() { 1 } else { usize::MAX }) - .map(AsUrl::as_url) + self.yanked.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }) } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index 008189a6..6358b200 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use hashbrown::HashSet; use indexmap::{IndexSet, set::MutableValues}; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, file::FileCov}; +use yazi_fs::{FilesOp, file::{File, FileCov}}; use yazi_macro::err; use yazi_shared::url::{Url, UrlBuf, UrlCov, UrlLike}; @@ -27,6 +27,8 @@ impl Yanked { Self { cut, files, ..Default::default() } } + pub fn files(&self) -> impl Iterator { self.files.iter().map(|f| &f.0) } + pub fn urls(&self) -> impl Iterator { self.files.iter().map(|f| &f.url) } pub fn remove_many<'a, I, T>(&mut self, urls: I) diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index e9b75d49..9df747c4 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -1,8 +1,8 @@ -use std::mem; +use std::{mem, ops::Deref}; use yazi_config::{LAYOUT, YAZI}; use yazi_dds::Pubsub; -use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File}; +use yazi_fs::{Entries, FilesOp, FolderStage, cha::ChaType, file::File}; use yazi_macro::err; use yazi_shared::{id::Id, path::{DynPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::{Scrollable, Step}; @@ -10,8 +10,7 @@ use yazi_widgets::{Scrollable, Step}; use crate::MgrProxy; pub struct Folder { - pub url: UrlBuf, - pub cha: Cha, + pub file: File, pub entries: Entries, pub stage: FolderStage, @@ -22,11 +21,16 @@ pub struct Folder { pub trace: Option, } +impl Deref for Folder { + type Target = File; + + fn deref(&self) -> &Self::Target { &self.file } +} + impl Default for Folder { fn default() -> Self { Self { - url: Default::default(), - cha: Default::default(), + file: Default::default(), entries: Entries::new(YAZI.mgr.show_hidden.get()), stage: Default::default(), offset: Default::default(), @@ -38,34 +42,36 @@ impl Default for Folder { } impl> From for Folder { - fn from(value: T) -> Self { Self { url: value.into(), ..Default::default() } } + fn from(value: T) -> Self { + Self { file: File::from_dummy(value, Some(ChaType::Dir)), ..Default::default() } + } } impl Folder { pub fn update(&mut self, op: FilesOp) -> bool { let (stage, revision) = (self.stage.clone(), self.entries.revision); match op { - FilesOp::Full(_, _, cha) => { - (self.cha, self.stage) = (cha, FolderStage::Loaded); + FilesOp::Full(ref file, _) => { + (self.file, self.stage) = (file.clone(), FolderStage::Loaded); } FilesOp::Part(_, ref files, _) if files.is_empty() => { - (self.cha, self.stage) = (Cha::default(), FolderStage::Loading); + self.stage = FolderStage::Loading; } FilesOp::Part(_, _, ticket) if ticket == self.entries.ticket() => { self.stage = FolderStage::Loading; } - FilesOp::Done(_, cha, ticket) if ticket == self.entries.ticket() => { - (self.cha, self.stage) = (cha, FolderStage::Loaded); + FilesOp::Done(ref file, ticket) if ticket == self.entries.ticket() => { + (self.file, self.stage) = (file.clone(), FolderStage::Loaded); } FilesOp::IOErr(_, ref err) => { - (self.cha, self.stage) = (Cha::default(), FolderStage::Failed(err.clone())); + self.stage = FolderStage::Failed(err.clone()); } _ => {} } let mut deleted = vec![]; match op { - FilesOp::Full(_, files, _) => self.entries.update_full(files), + FilesOp::Full(_, files) => self.entries.update_full(files), FilesOp::Part(_, files, ticket) => self.entries.update_part(files, ticket), FilesOp::Done(..) => {} FilesOp::Size(_, sizes) => self.entries.update_size(sizes), diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index da4a2f0b..0a1bcec8 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -1,14 +1,10 @@ -use std::time::Duration; - -use tokio::{pin, task::JoinHandle}; -use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use tokio::task::JoinHandle; use yazi_adapter::ADAPTOR; use yazi_config::{LAYOUT, YAZI}; -use yazi_fs::{Entries, FilesOp, cha::Cha, file::File}; +use yazi_fs::file::File; use yazi_macro::render; use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}}; use yazi_shared::{pool::Symbol, url::UrlBuf}; -use yazi_vfs::{VfsEntries, VfsFilesOp}; use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock}; @@ -17,9 +13,7 @@ pub struct Preview { pub lock: Option, pub skip: usize, - handle: Option>, - pub folder_lock: Option, - folder_loader: Option>, + handle: Option>, } impl Preview { @@ -47,36 +41,6 @@ impl Preview { })); } - pub fn go_folder(&mut self, file: File, dir: Option, mime: Symbol, force: bool) { - if self.folder_lock.as_ref() == Some(&file.url) { - return self.go(file, mime, force); - } - - let wd = file.url_owned(); - self.go(file, mime, force); - - self.folder_lock = Some(wd.clone()); - self.folder_loader.take().map(|h| h.abort()); - self.folder_loader = Some(tokio::spawn(async move { - let Some(new) = Entries::assert_stale(&wd, dir.unwrap_or_default()).await else { return }; - - let rx = match Entries::from_dir(&wd).await { - Ok(rx) => rx, - Err(e) => return FilesOp::issue_error(&wd, e).await, - }; - - let stream = - UnboundedReceiverStream::new(rx).chunks_timeout(50000, Duration::from_millis(500)); - pin!(stream); - - let ticket = FilesOp::prepare(&wd); - while let Some(chunk) = stream.next().await { - FilesOp::Part(wd.clone(), chunk, ticket).emit(); - } - FilesOp::Done(wd, new, ticket).emit(); - })); - } - pub fn abort(&mut self) { self.handle.take().map(|ct| ct.abort()); Highlighter::abort(); @@ -103,6 +67,4 @@ impl Preview { pub fn same_lock(&self, file: &File, mime: &str) -> bool { self.same_file(file, mime) && matches!(&self.lock, Some(l) if l.skip == self.skip) } - - pub fn same_folder(&self, url: &UrlBuf) -> bool { self.folder_lock.as_ref() == Some(url) } } diff --git a/yazi-core/src/tab/snap.rs b/yazi-core/src/tab/snap.rs index 5d59bfd2..e21f68bb 100644 --- a/yazi-core/src/tab/snap.rs +++ b/yazi-core/src/tab/snap.rs @@ -1,17 +1,14 @@ -use yazi_shared::url::UrlBuf; +use yazi_fs::file::File; use crate::tab::Tab; pub struct TabSnap { - pub hovered: Option, - pub selected: Vec, + pub hovered: Option, + pub selected: Vec, } impl From<&Tab> for TabSnap { fn from(value: &Tab) -> Self { - Self { - hovered: value.hovered_url().cloned(), - selected: value.selected.urls().cloned().collect(), - } + Self { hovered: value.hovered().cloned(), selected: value.selected.files().cloned().collect() } } } diff --git a/yazi-core/src/tasks/prework.rs b/yazi-core/src/tasks/prework.rs index 23ec2ca5..5f3ff692 100644 --- a/yazi-core/src/tasks/prework.rs +++ b/yazi-core/src/tasks/prework.rs @@ -1,5 +1,5 @@ use yazi_config::{YAZI, plugin::MAX_FETCHERS}; -use yazi_fs::{Entries, FsHash64, SortBy, file::File}; +use yazi_fs::{Entries, FsHash64, SortBy, file::{File, FileSig}}; use super::Tasks; use crate::mgr::Mimetype; @@ -9,7 +9,7 @@ impl Tasks { let mut loaded = self.scheduler.fetch.loaded.lock(); let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default(); for f in paged { - let hash = f.hash_u64(); + let hash = FileSig(f).hash_u64(); for g in YAZI.plugin.fetchers.matches(f, mimetype.get(&f.url).unwrap_or_default()) { match loaded.get_mut(&hash) { Some(n) if *n & (1 << g.idx) != 0 => continue, @@ -32,7 +32,7 @@ impl Tasks { pub fn preload_paged(&self, paged: &[File], mimetype: &Mimetype) { let mut loaded = self.scheduler.preload.loaded.lock(); for f in paged { - let hash = f.hash_u64(); + let hash = FileSig(f).hash_u64(); for p in YAZI.plugin.preloaders.matches(f, mimetype.get(&f.url).unwrap_or_default()) { match loaded.get_mut(&hash) { Some(n) if *n & (1 << p.idx) != 0 => continue, diff --git a/yazi-ffi/Cargo.toml b/yazi-ffi/Cargo.toml index 9acff995..2ffbd799 100644 --- a/yazi-ffi/Cargo.toml +++ b/yazi-ffi/Cargo.toml @@ -24,3 +24,6 @@ libc = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] core-foundation-sys = { workspace = true } objc2 = { workspace = true } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = [ "Win32_System_Com" ] } diff --git a/yazi-ffi/src/com.rs b/yazi-ffi/src/com.rs new file mode 100644 index 00000000..c9aa1cbc --- /dev/null +++ b/yazi-ffi/src/com.rs @@ -0,0 +1,16 @@ +use std::io; + +use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx, CoUninitialize}; + +pub struct Com; + +impl Com { + pub fn new() -> io::Result { + unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok().map_err(io::Error::other)? }; + Ok(Self) + } +} + +impl Drop for Com { + fn drop(&mut self) { unsafe { CoUninitialize() } } +} diff --git a/yazi-ffi/src/lib.rs b/yazi-ffi/src/lib.rs index 182356db..aea5ea5e 100644 --- a/yazi-ffi/src/lib.rs +++ b/yazi-ffi/src/lib.rs @@ -1,2 +1,5 @@ #[cfg(target_os = "macos")] yazi_macro::mod_flat!(cf_dict cf_string disk_arbitration io_kit); + +#[cfg(windows)] +yazi_macro::mod_flat!(com); diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index 973ec3ea..152162f4 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -32,9 +32,9 @@ impl<'a> Input<'a> { THEME.icon.matches( &File { - url: Path::new(path).into(), - cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() }, - link_to: None, + url: Path::new(path).into(), + cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() }, + extra: Default::default(), }, is_hovered, ) diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 076fdc97..b6d29768 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -23,6 +23,7 @@ yazi-shim = { path = "../yazi-shim", version = "26.5.6" } anyhow = { workspace = true } arc-swap = { workspace = true } bitflags = { workspace = true } +data-encoding = { workspace = true } dirs = { workspace = true } either = { workspace = true } foldhash = { workspace = true } @@ -31,11 +32,11 @@ inventory = { workspace = true } libc = { workspace = true } mlua = { workspace = true } parking_lot = { workspace = true } -percent-encoding = { workspace = true } rand = { workspace = true } regex = { workspace = true } scopeguard = { workspace = true } serde = { workspace = true } +serde_with = { workspace = true } strum = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -45,10 +46,12 @@ typed-path = { workspace = true } uzers = { workspace = true } [target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_UI_Shell_PropertiesSystem" ] } windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] } [target.'cfg(target_os = "macos")'.dependencies] core-foundation-sys = { workspace = true } +ds_parser = "0.2.1" objc2 = { workspace = true } [target.'cfg(not(target_os = "android"))'.dependencies] diff --git a/yazi-fs/src/auth.rs b/yazi-fs/src/auth.rs new file mode 100644 index 00000000..5c4e958b --- /dev/null +++ b/yazi-fs/src/auth.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; + +use yazi_shared::{auth::{Auth, AuthKind}, path::PathBufDyn, spec::SpecInventory}; +use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; + +use crate::{FsHash128, Xdg}; + +pub trait FsAuth { + fn cache_root(&self) -> Option; + + fn stamp_root(&self) -> Option { + self.cache_root().map(|mut root| { + root.push("%stamp"); + root + }) + } +} + +impl FsAuth for Auth { + fn cache_root(&self) -> Option { + match self.kind { + AuthKind::Regular | AuthKind::Search => None, + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { + Some(Xdg::temp_dir().join(format!( + "{}_{}_{}", + self.kind.into_str(), + self.scheme, + self.domain.hash_u128_str(&mut [0; 26]) + ))) + } + } + } +} + +// --- Inject +inventory::submit! { + SpecInventory { + register: |registry| { + registry.add_cached_field("cache", |_, me| Ok(me.cache_root().map(PathBufDyn::from))); + registry.add_cached_field("stamp", |_, me| Ok(me.stamp_root().map(PathBufDyn::from))); + } + } +} diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 0ee64e82..e5d0d897 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -131,6 +131,16 @@ impl Cha { self.kind |= kind; self } + + #[inline] + pub fn follow(self, followed: Option) -> Self { + if !self.is_link() { + return self; + } + + let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM) | ChaKind::FOLLOW; + followed.unwrap_or(self).attach(retain) + } } impl Cha { diff --git a/yazi-fs/src/cha/cha_sig.rs b/yazi-fs/src/cha/cha_sig.rs new file mode 100644 index 00000000..f54ab024 --- /dev/null +++ b/yazi-fs/src/cha/cha_sig.rs @@ -0,0 +1,21 @@ +use std::{hash::{Hash, Hasher}, ops::Deref}; + +use crate::cha::Cha; + +#[derive(Clone, Copy, Debug)] +pub struct ChaSig(pub Cha); + +impl Deref for ChaSig { + type Target = Cha; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Hash for ChaSig { + fn hash(&self, state: &mut H) { + self.len.hash(state); + self.btime.hash(state); + self.ctime.hash(state); + self.mtime.hash(state); + } +} diff --git a/yazi-fs/src/cha/lua.rs b/yazi-fs/src/cha/lua.rs index 5a761cf9..6a1be252 100644 --- a/yazi-fs/src/cha/lua.rs +++ b/yazi-fs/src/cha/lua.rs @@ -67,17 +67,17 @@ impl UserData for Cha { } fn add_methods>(methods: &mut M) { - methods.add_method("hash", |_, me, long: Option| { - Ok(if long.unwrap_or(false) { - format!("{:x}", me.hash_u128()) + methods.add_method("hash", |lua, me, long: bool| { + Ok(if long { + lua.create_string(me.hash_u128_str(&mut [0; 26])) } else { Err("Short hash not supported".into_lua_err())? }) }); - methods.add_method("perm", |lua, _me, ()| { + methods.add_method("perm", |_lua, _me, ()| { Ok( #[cfg(unix)] - lua.create_string(_me.mode.permissions(_me.is_dummy())), + _lua.create_string(_me.mode.permissions(_me.is_dummy())), #[cfg(windows)] Ok::<_, mlua::Error>(mlua::Value::Nil), ) diff --git a/yazi-fs/src/cha/mod.rs b/yazi-fs/src/cha/mod.rs index 75d67400..18819389 100644 --- a/yazi-fs/src/cha/mod.rs +++ b/yazi-fs/src/cha/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(cha kind lua mode r#type); +yazi_macro::mod_flat!(cha cha_sig kind lua mode r#type); diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index 104ea125..3f962cb6 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -30,7 +30,7 @@ impl Default for Cwd { } impl Cwd { - pub fn path(&self) -> PathBuf { self.0.load().as_url().unified_path().into_owned() } + pub fn path(&self) -> PathBuf { self.0.load().as_url().working_path().into_owned() } pub fn set(&self, url: &UrlBuf, callback: fn()) -> bool { if !url.is_absolute() { @@ -48,8 +48,8 @@ impl Cwd { pub fn ensure(url: Url) -> Cow { use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C}; - let Some(cache) = url.cache() else { - return url.unified_path(); + let Some(cache) = url.cache_bucket() else { + return url.working_path(); }; if !matches!(std::fs::create_dir_all(&cache), Err(e) if e.kind() == NotADirectory || e.kind() == AlreadyExists) diff --git a/yazi-fs/src/engine/local/dir_entry.rs b/yazi-fs/src/engine/local/dir_entry.rs index fb494640..2b0b6f74 100644 --- a/yazi-fs/src/engine/local/dir_entry.rs +++ b/yazi-fs/src/engine/local/dir_entry.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; -use crate::{cha::{Cha, ChaType}, engine::FileHolder}; +use crate::{cha::{Cha, ChaType}, engine::FileHolder, file::{File, FileExtra}}; pub enum DirEntry { Regular(tokio::fs::DirEntry), @@ -10,6 +10,21 @@ pub enum DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { + let cha = self.metadata().await?; + let url = self.url(); + + let (mut followed, mut link_to) = (None, None); + if cha.is_link() { + let path = url.as_local().expect("local entry path"); + let name = path.file_name().unwrap_or_default(); + followed = tokio::fs::metadata(path).await.ok().map(|m| Cha::new(name, m)); + link_to = tokio::fs::read_link(path).await.ok().map(Into::into); + } + + Ok(File { url, cha: cha.follow(followed), extra: FileExtra::new(link_to, None) }) + } + async fn file_type(&self) -> io::Result { match self { Self::Regular(entry) | Self::Others { entry, .. } => entry.file_type().await.map(Into::into), diff --git a/yazi-fs/src/engine/traits.rs b/yazi-fs/src/engine/traits.rs index 6f42e63e..4ee100fe 100644 --- a/yazi-fs/src/engine/traits.rs +++ b/yazi-fs/src/engine/traits.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::{DynPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; -use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}}; +use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}, file::{File, FileExtra}}; pub trait Engine: Sized { type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; @@ -78,6 +78,24 @@ pub trait Engine: Sized { fn demand(&self) -> Self::Demand { Self::Demand::default() } + fn file(&self) -> impl Future> { + async move { + let cha = self.symlink_metadata().await?; + + let (mut followed, mut link_to) = (None, None); + if cha.is_link() { + followed = self.metadata().await.ok(); + link_to = self.read_link().await.ok(); + } + + Ok(File { + url: self.url().to_owned(), + cha: cha.follow(followed), + extra: FileExtra::new(link_to, None), + }) + } + } + fn hard_link

(&self, to: P) -> impl Future> where P: DynPath; @@ -94,6 +112,13 @@ pub trait Engine: Sized { fn read_link(&self) -> impl Future>; + fn revalidate(&self, file: File) -> impl Future>> { + async move { + let new = self.file().await?; + if new.cha.hits(file.cha) { Ok(None) } else { Ok(Some(new)) } + } + } + fn remove_dir(&self) -> impl Future>; fn remove_dir_all(&self) -> impl Future> { @@ -204,6 +229,9 @@ pub trait DirReader { // --- FileHolder pub trait FileHolder { + #[must_use] + fn file(&self) -> impl Future>; + #[must_use] fn file_type(&self) -> impl Future>; diff --git a/yazi-fs/src/entries.rs b/yazi-fs/src/entries.rs index 86ac0796..1151da9f 100644 --- a/yazi-fs/src/entries.rs +++ b/yazi-fs/src/entries.rs @@ -292,25 +292,20 @@ impl Entries { // --- Show hidden pub fn set_show_hidden(&mut self, state: bool) { - if self.show_hidden == state { - return; - } - - self.show_hidden = state; - if self.show_hidden && self.hidden.is_empty() { - return; - } else if !self.show_hidden && self.items.is_empty() { + if mem::replace(&mut self.show_hidden, state) == state { return; } + let len = self.items.len(); let take = if self.show_hidden { mem::take(&mut self.hidden) } else { mem::take(&mut self.items) }; - let (hidden, items) = self.split_files(take); - - self.hidden.extend(hidden); - if !items.is_empty() { - self.revision += 1; - self.items.extend(items); + if take.is_empty() { + return; } + + let (hidden, items) = self.split_files(take); + self.hidden.extend(hidden); + self.items.extend(items); + self.revision += (self.items.len() != len) as u64; } } diff --git a/yazi-fs/src/file/file.rs b/yazi-fs/src/file/file.rs index 58a97779..9f6e2b32 100644 --- a/yazi-fs/src/file/file.rs +++ b/yazi-fs/src/file/file.rs @@ -1,15 +1,16 @@ -use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path}; +use std::{borrow::Cow, ops::Deref, path::{Path, PathBuf}}; use serde::{Deserialize, Serialize}; -use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}}; +use yazi_shared::{path::PathDyn, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}}; -use crate::cha::{Cha, ChaType}; +use crate::{FsUrl, cha::{Cha, ChaType}, file::FileExtra}; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct File { - pub url: UrlBuf, - pub cha: Cha, - pub link_to: Option, + pub url: UrlBuf, + pub cha: Cha, + #[serde(flatten)] + pub extra: FileExtra, } impl Deref for File { @@ -39,16 +40,32 @@ impl AsUrl for &File { } impl File { + #[inline] + pub fn cache(&self) -> Option { + if self.is_dir() { self.url.cache_bucket() } else { self.url.cache_entry() } + } + #[inline] pub fn from_dummy(url: impl Into, r#type: Option) -> Self { let url = url.into(); let cha = Cha::from_dummy(&url, r#type); - Self { url, cha, link_to: None } + Self { url, cha, extra: Default::default() } } #[inline] pub fn chdir(&self, wd: &Path) -> Self { - Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } + Self { url: self.url.rebase(wd), cha: self.cha, extra: self.extra.clone() } + } + + #[inline] + pub fn content_path(&self) -> Cow<'_, Path> { + if let Some(backing) = self.extra.backing() { + backing.into() + } else if let Some(local) = self.url.as_local() { + local.into() + } else { + self.cache().expect("non-local URL should have a cache path").into() + } } } @@ -72,13 +89,3 @@ impl File { #[inline] pub fn stem(&self) -> Option> { self.url.stem() } } - -impl Hash for File { - fn hash(&self, state: &mut H) { - self.url.hash(state); - self.cha.len.hash(state); - self.cha.btime.hash(state); - self.cha.ctime.hash(state); - self.cha.mtime.hash(state); - } -} diff --git a/yazi-fs/src/file/file_extra.rs b/yazi-fs/src/file/file_extra.rs new file mode 100644 index 00000000..2eb8ea9f --- /dev/null +++ b/yazi-fs/src/file/file_extra.rs @@ -0,0 +1,67 @@ +use std::{path::{Path, PathBuf}, sync::Arc}; + +use mlua::{FromLua, Lua, Table, Value}; +use serde::{Deserialize, Serialize}; +use serde_with::{TryFromInto, serde_as}; +use yazi_shared::path::PathBufDyn; + +#[repr(transparent)] +#[derive(Clone, Debug, Default)] +pub struct FileExtra(Option>); + +#[serde_as] +#[derive(Debug, Default, Deserialize, Serialize)] +struct FileExtraInner { + link_to: Option, + #[serde_as(as = "Option>")] + backing: Option, +} + +impl FileExtra { + #[inline] + pub fn new(link_to: Option, backing: Option) -> Self { + Self( + (link_to.is_some() || backing.is_some()) + .then(|| Arc::new(FileExtraInner { link_to, backing })), + ) + } + + #[inline] + pub fn link_to(&self) -> Option<&PathBufDyn> { self.0.as_ref()?.link_to.as_ref() } + + #[inline] + pub fn backing(&self) -> Option<&Path> { self.0.as_ref()?.backing.as_deref() } +} + +impl TryFrom

for FileExtra { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + Ok(Self::new( + value.raw_get("link_to")?, + value.raw_get::>("backing")?.map(PathBufDyn::into_os).transpose()?, + )) + } +} + +impl Serialize for FileExtra { + fn serialize(&self, serializer: S) -> Result { + match &self.0 { + Some(inner) => inner.serialize(serializer), + None => FileExtraInner::default().serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for FileExtra { + fn deserialize>(deserializer: D) -> Result { + let inner = FileExtraInner::deserialize(deserializer)?; + Ok(Self::new(inner.link_to, inner.backing)) + } +} + +impl FromLua for FileExtra { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + Table::from_lua(value, lua)?.try_into() + } +} diff --git a/yazi-fs/src/file/file_sig.rs b/yazi-fs/src/file/file_sig.rs new file mode 100644 index 00000000..dd3b8db6 --- /dev/null +++ b/yazi-fs/src/file/file_sig.rs @@ -0,0 +1,26 @@ +use std::{hash::{Hash, Hasher}, ops::Deref}; + +use yazi_shared::url::Url; + +use crate::{cha::ChaSig, file::File}; + +#[derive(Clone, Copy, Debug)] +pub struct FileSig<'a>(pub &'a File); + +impl Deref for FileSig<'_> { + type Target = File; + + fn deref(&self) -> &Self::Target { self.0 } +} + +impl Hash for FileSig<'_> { + fn hash(&self, state: &mut H) { + if let Some(backing) = self.extra.backing() { + Url::regular(backing).hash(state); + } else { + self.url.hash(state); + } + + ChaSig(self.cha).hash(state); + } +} diff --git a/yazi-fs/src/file/lua.rs b/yazi-fs/src/file/lua.rs index 32d19bf4..a3003f0f 100644 --- a/yazi-fs/src/file/lua.rs +++ b/yazi-fs/src/file/lua.rs @@ -15,7 +15,11 @@ impl TryFrom
for File { type Error = mlua::Error; fn try_from(value: Table) -> Result { - Ok(Self { url: value.raw_get("url")?, cha: value.raw_get("cha")?, ..Default::default() }) + Ok(Self { + url: value.raw_get("url")?, + cha: value.raw_get("cha")?, + extra: value.try_into()?, + }) } } diff --git a/yazi-fs/src/file/mod.rs b/yazi-fs/src/file/mod.rs index 7e2916c3..4e7de598 100644 --- a/yazi-fs/src/file/mod.rs +++ b/yazi-fs/src/file/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(data file_cov file file_ref files inventory lua); +yazi_macro::mod_flat!(data file file_cov file_extra file_ref file_sig files inventory lua); diff --git a/yazi-fs/src/hash.rs b/yazi-fs/src/hash.rs index 16df2a42..33a29435 100644 --- a/yazi-fs/src/hash.rs +++ b/yazi-fs/src/hash.rs @@ -1,26 +1,58 @@ use std::hash::{BuildHasher, Hash}; +use data_encoding::BASE32_NOPAD; use mlua::UserDataMethods; -use yazi_shared::url::{AsUrl, UrlBuf, UrlBufInventory}; +use yazi_shared::{auth::Domain, url::{AsUrl, Url, UrlBuf, UrlBufInventory}}; use yazi_shim::Twox128; -use crate::{cha::Cha, file::File}; +use crate::{cha::Cha, file::FileSig}; pub trait FsHash64 { fn hash_u64(&self) -> u64; -} -impl FsHash64 for File { - fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } + fn hash_u64_str<'a>(&self, buf: &'a mut [u8; 13]) -> &'a str { + BASE32_NOPAD.encode_mut_str(&self.hash_u64().to_be_bytes(), buf) + } } impl FsHash64 for UrlBuf { fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) } } +impl FsHash64 for FileSig<'_> { + fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } +} + // Hash128 pub trait FsHash128 { fn hash_u128(&self) -> u128; + + fn hash_u128_str<'a>(&self, buf: &'a mut [u8; 26]) -> &'a str { + BASE32_NOPAD.encode_mut_str(&self.hash_u128().to_be_bytes(), buf) + } +} + +impl FsHash128 for Url<'_> { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.auth().hash(&mut h); + for c in self.loc().components() { + c.hash(&mut h); + } + h.finish_128() + } +} + +impl FsHash128 for UrlBuf { + fn hash_u128(&self) -> u128 { self.as_url().hash_u128() } +} + +impl FsHash128 for Domain<'_> { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.hash(&mut h); + h.finish_128() + } } impl FsHash128 for Cha { @@ -35,10 +67,10 @@ impl FsHash128 for Cha { } } -impl FsHash128 for T { +impl FsHash128 for FileSig<'_> { fn hash_u128(&self) -> u128 { let mut h = Twox128::default(); - self.as_url().hash(&mut h); + self.hash(&mut h); h.finish_128() } } @@ -47,11 +79,11 @@ impl FsHash128 for T { inventory::submit! { UrlBufInventory { register: |registry| { - registry.add_method("hash", |_, me, long: bool| { + registry.add_method("hash", |lua, me, long: bool| { Ok(if long { - format!("{:x}", me.hash_u128()) + lua.create_string(me.hash_u128_str(&mut [0; 26])) } else { - format!("{:x}", me.hash_u64()) + lua.create_string(me.hash_u64_str(&mut [0; 13])) }) }); } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 666cd11b..c9987963 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,8 +1,8 @@ extern crate self as yazi_fs; -yazi_macro::mod_pub!(cha file mounts path engine); +yazi_macro::mod_pub!(cha file mounts path engine trash); -yazi_macro::mod_flat!(cwd spec entries filter fns hash op sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(auth cwd entries filter fns hash op sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default()); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index e75733b6..f9321016 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -4,15 +4,15 @@ use hashbrown::{HashMap, HashSet}; use yazi_macro::{impl_data_any, relay}; use yazi_shared::{id::{Id, Ids}, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike, UrlMapExt}}; -use crate::{cha::Cha, file::File}; +use crate::file::File; pub static FILES_TICKET: Ids = Ids::new(); #[derive(Clone, Debug)] pub enum FilesOp { - Full(UrlBuf, Vec, Cha), + Full(File, Vec), Part(UrlBuf, Vec, Id), - Done(UrlBuf, Cha, Id), + Done(File, Id), Size(UrlBuf, HashMap), IOErr(UrlBuf, yazi_shim::fs::Error), @@ -27,9 +27,9 @@ impl_data_any!(FilesOp); impl FilesOp { pub fn cwd(&self) -> &UrlBuf { match self { - Self::Full(u, ..) => u, + Self::Full(f, ..) => &f.url, Self::Part(u, ..) => u, - Self::Done(u, ..) => u, + Self::Done(f, ..) => &f.url, Self::Size(u, _) => u, Self::IOErr(u, _) => u, @@ -42,7 +42,7 @@ impl FilesOp { pub fn files(&self) -> Box + '_> { match self { - Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => { + Self::Full(_, files) | Self::Part(_, files, _) | Self::Creating(_, files) => { Box::new(files.iter().filter(|f| !f.entry_key().is_empty())) } Self::Done(..) => Box::new(iter::empty()), @@ -141,9 +141,9 @@ impl FilesOp { let w = UrlBuf::from(wd); match self { - Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), + Self::Full(file, files) => Self::Full(file.chdir(wd), files!(files)), Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), - Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket), + Self::Done(file, ticket) => Self::Done(file.chdir(wd), *ticket), Self::Size(_, map) => Self::Size(w, map.iter().map(|(urn, &s)| (urn.clone(), s)).collect()), Self::IOErr(_, err) => Self::IOErr(w, err.clone()), diff --git a/yazi-fs/src/path/mod.rs b/yazi-fs/src/path/mod.rs index a3b70bd8..6aee3c6e 100644 --- a/yazi-fs/src/path/mod.rs +++ b/yazi-fs/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(clean expand normalize path percent relative); +yazi_macro::mod_flat!(clean expand normalize path relative); diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs deleted file mode 100644 index 2b5358da..00000000 --- a/yazi-fs/src/path/percent.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::{borrow::Cow, path::{Path, PathBuf}}; - -use anyhow::Result; -use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; -use yazi_shared::path::{PathCow, PathDyn, PathKind}; - -const SET: &AsciiSet = &CONTROLS - .add(b'"') - .add(b'*') - .add(b':') - .add(b'<') - .add(b'>') - .add(b'?') - .add(b'\\') - .add(b'|') - .add(b'%'); - -pub trait PercentEncoding<'a> { - fn percent_encode(self) -> Cow<'a, Path>; - - fn percent_decode(self, kind: K) -> Result> - where - K: Into; -} - -impl<'a> PercentEncoding<'a> for PathDyn<'a> { - fn percent_encode(self) -> Cow<'a, Path> { - match percent_encode(self.encoded_bytes(), SET).into() { - Cow::Borrowed(s) => Path::new(s).into(), - Cow::Owned(s) => PathBuf::from(s).into(), - } - } - - fn percent_decode(self, kind: K) -> Result> - where - K: Into, - { - PathCow::with(kind, percent_decode(self.encoded_bytes())) - } -} diff --git a/yazi-fs/src/spec.rs b/yazi-fs/src/spec.rs deleted file mode 100644 index 6768e29a..00000000 --- a/yazi-fs/src/spec.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::path::PathBuf; - -use yazi_shared::{auth::{Auth, AuthKind, EncodeAuth}, path::PathBufDyn, spec::SpecInventory}; -use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; - -use crate::Xdg; - -pub trait FsSpec { - fn cache(&self) -> Option; -} - -impl FsSpec for Auth { - fn cache(&self) -> Option { - match self.kind { - AuthKind::Regular | AuthKind::Search => None, - AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { - Some(Xdg::temp_dir().join(format!( - "{}_{}_{}", - self.kind.into_str(), - self.scheme, - EncodeAuth::domain(&self.domain) - ))) - } - } - } -} - -// --- Inject -inventory::submit! { - SpecInventory { - register: |registry| { - registry.add_cached_field("cache", |_, me| Ok(me.cache().map(PathBufDyn::from))); - } - } -} diff --git a/yazi-fs/src/splatter.rs b/yazi-fs/src/splatter.rs index 42cd4af5..c2db5bce 100644 --- a/yazi-fs/src/splatter.rs +++ b/yazi-fs/src/splatter.rs @@ -2,11 +2,11 @@ use std::os::unix::ffi::{OsStrExt, OsStringExt}; #[cfg(windows)] use std::os::windows::ffi::{OsStrExt, OsStringExt}; -use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem}; +use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem, path::Path}; -use yazi_shared::url::{AsUrl, Url}; +use yazi_shared::url::UrlLike; -use crate::FsUrl; +use crate::file::File; #[cfg(unix)] type Iter<'a> = Peekable>>; @@ -27,11 +27,11 @@ pub struct Splatter { pub trait Splatable { fn tab(&self) -> usize; - fn selected(&self, tab: usize, idx: Option) -> impl Iterator>; + fn selected(&self, tab: usize, idx: Option) -> impl Iterator; - fn hovered(&self, tab: usize) -> Option>; + fn hovered(&self, tab: usize) -> Option<&File>; - fn yanked(&self, idx: Option) -> impl Iterator>; + fn yanked(&self, idx: Option) -> impl Iterator; } #[cfg(unix)] @@ -91,15 +91,15 @@ where let idx = self.consume_digit(it); let mut first = true; - for url in self.src.selected(self.tab, idx) { + for file in self.src.selected(self.tab, idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } if c == Some('S') { - cue(buf, url.os_str()); + cue(buf, file.url.os_str()); } else { - cue(buf, url.unified_path_str()); + cue(buf, &*file.content_path()); } } if first && idx.is_some() { @@ -110,10 +110,10 @@ where fn visit_hovered(&mut self, it: &mut Iter, buf: &mut Buf) { match it.next().and_then(b2c) { Some('h') => { - cue(buf, self.src.hovered(self.tab).map(|u| u.unified_path_str()).unwrap_or_default()); + cue(buf, &*self.src.hovered(self.tab).map(|f| f.content_path()).unwrap_or_default()); } Some('H') => { - cue(buf, self.src.hovered(self.tab).map(|u| u.os_str()).unwrap_or_default()); + cue(buf, self.src.hovered(self.tab).map(|f| f.url.os_str()).unwrap_or_default()); } _ => unreachable!(), } @@ -124,15 +124,15 @@ where let idx = self.consume_digit(it); let mut first = true; - for url in self.src.selected(self.tab, idx) { + for file in self.src.selected(self.tab, idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } if c == Some('D') { - cue(buf, url.parent().map(|p| p.os_str()).unwrap_or_default()); + cue(buf, file.url.parent().map(|p| p.os_str()).unwrap_or_default()); } else { - cue(buf, url.parent().map(|p| p.unified_path_str()).unwrap_or_default()); + cue(buf, file.content_path().parent().unwrap_or(Path::new(""))); } } if first && idx.is_some() { @@ -157,15 +157,15 @@ where let idx = self.consume_digit(it); let mut first = true; - for url in self.src.yanked(idx) { + for file in self.src.yanked(idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } if c == Some('Y') { - cue(buf, url.os_str()); + cue(buf, file.url.os_str()); } else { - cue(buf, url.unified_path_str()); + cue(buf, &*file.content_path()); } } if first && idx.is_some() { @@ -204,16 +204,16 @@ impl Splatter { impl Splatable for &Source { fn tab(&self) -> usize { 0 } - fn selected(&self, _tab: usize, idx: Option) -> impl Iterator> { + fn selected(&self, _tab: usize, idx: Option) -> impl Iterator { if idx.is_none() { self.0.set(true); } iter::empty() } - fn hovered(&self, _tab: usize) -> Option> { None } + fn hovered(&self, _tab: usize) -> Option<&File> { None } - fn yanked(&self, _idx: Option) -> impl Iterator> { iter::empty() } + fn yanked(&self, _idx: Option) -> impl Iterator { iter::empty() } } let src = Source(Cell::new(false)); @@ -222,68 +222,64 @@ impl Splatter { } } -impl<'a, I, T> Splatable for &'a I +impl Splatable for &I where I: ?Sized, - &'a I: IntoIterator, - T: AsUrl + 'a, + for<'a> &'a I: IntoIterator, { fn tab(&self) -> usize { 1 } - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); - (*self) - .into_iter() - .filter(move |_| tab == 1) - .map(|u| u.as_url()) - .skip(idx.unwrap_or(0)) - .take(if idx.is_some() { 1 } else { usize::MAX }) + (*self).into_iter().filter(move |_| tab == 1).skip(idx.unwrap_or(0)).take(if idx.is_some() { + 1 + } else { + usize::MAX + }) } - fn hovered(&self, _tab: usize) -> Option> { None } + fn hovered(&self, _tab: usize) -> Option<&File> { None } - fn yanked(&self, _idx: Option) -> impl Iterator> { iter::empty() } + fn yanked(&self, _idx: Option) -> impl Iterator { iter::empty() } } #[cfg(test)] mod tests { + use std::sync::LazyLock; + use super::*; struct Source(usize); + static SELECTED: LazyLock<[Vec; 2]> = + LazyLock::new(|| [vec![file("t1/s1"), file("t1/s2")], vec![file("t 2/s 1"), file("t 2/s 2")]]); + static HOVERED: LazyLock<[File; 2]> = LazyLock::new(|| [file("hovered"), file("hover ed")]); + static YANKED: LazyLock<[File; 3]> = LazyLock::new(|| [file("y1"), file("y 2"), file("y3")]); + + fn file(path: &'static str) -> File { File::from_dummy(Path::new(path), None) } + impl Splatable for Source { fn tab(&self) -> usize { self.0 } - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { - let urls = if tab == 1 { - vec![Url::regular("t1/s1"), Url::regular("t1/s2")] - } else if tab == 2 { - vec![Url::regular("t 2/s 1"), Url::regular("t 2/s 2")] - } else { - vec![] - }; - + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); - urls.into_iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }) - } - - fn hovered(&self, tab: usize) -> Option> { - if tab == 1 { - Some(Url::regular("hovered")) - } else if tab == 2 { - Some(Url::regular("hover ed")) - } else { - None - } - } - - fn yanked(&self, mut idx: Option) -> impl Iterator> { - idx = idx.and_then(|i| i.checked_sub(1)); - [Url::regular("y1"), Url::regular("y 2"), Url::regular("y3")] + tab + .checked_sub(1) + .and_then(|tab| SELECTED.get(tab)) .into_iter() + .flatten() .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) } + + fn hovered(&self, tab: usize) -> Option<&File> { + tab.checked_sub(1).and_then(|tab| HOVERED.get(tab)) + } + + fn yanked(&self, mut idx: Option) -> impl Iterator { + idx = idx.and_then(|i| i.checked_sub(1)); + YANKED.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }) + } } #[test] @@ -332,4 +328,19 @@ mod tests { assert_eq!(s, OsStr::new(expected), "{cmd}"); } } + + #[test] + #[cfg(unix)] + fn test_content_path() { + use crate::file::FileExtra; + + let file = File { + url: Path::new("/logical/file").into(), + cha: Default::default(), + extra: FileExtra::new(None, Some("/real/file".into())), + }; + + let s = Splatter::new(&[file]).splat(OsStr::new("%s %S %d %D")); + assert_eq!(s, OsStr::new("/real/file /logical/file /real /logical")); + } } diff --git a/yazi-fs/src/trash/entry.rs b/yazi-fs/src/trash/entry.rs new file mode 100644 index 00000000..717f6717 --- /dev/null +++ b/yazi-fs/src/trash/entry.rs @@ -0,0 +1,53 @@ +use std::{ffi::OsString, path::PathBuf}; + +use mlua::{IntoLua, Lua, Value}; +use yazi_shared::path::PathBufDyn; + +use crate::cha::Cha; + +#[derive(Clone, Debug)] +pub struct TrashEntry { + pub name: OsString, + pub key: OsString, + pub cha: Cha, + pub link_to: Option, + pub backing: PathBuf, +} + +impl TrashEntry { + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + pub(super) fn new(path: PathBuf, name: OsString, key: OsString) -> std::io::Result { + use super::TrashCha; + + let cha = Cha::from_trash(&path, &name, true)?; + let link_to = if cha.is_link() { std::fs::read_link(&path).ok() } else { None }; + + Ok(Self { name, key, cha, link_to, backing: path }) + } + + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + pub(super) fn into_file(self, url: impl Into) -> crate::file::File { + use crate::file::{File, FileExtra}; + + File { + url: url.into(), + cha: self.cha, + extra: FileExtra::new(self.link_to.map(Into::into), Some(self.backing)), + } + } +} + +impl IntoLua for TrashEntry { + fn into_lua(self, lua: &Lua) -> mlua::Result { + let Self { name, key, cha, link_to, backing } = self; + lua + .create_table_from([ + ("name", lua.create_external_string(name.into_encoded_bytes())?.into_lua(lua)?), + ("key", lua.create_external_string(key.into_encoded_bytes())?.into_lua(lua)?), + ("cha", cha.into_lua(lua)?), + ("link_to", link_to.map(PathBufDyn::Os).into_lua(lua)?), + ("backing", PathBufDyn::Os(backing).into_lua(lua)?), + ])? + .into_lua(lua) + } +} diff --git a/yazi-fs/src/trash/freedesktop.rs b/yazi-fs/src/trash/freedesktop.rs new file mode 100644 index 00000000..ec6a2945 --- /dev/null +++ b/yazi-fs/src/trash/freedesktop.rs @@ -0,0 +1,171 @@ +use std::{ffi::OsStr, fs, hash::Hash, io, path::{Path, PathBuf}}; + +use hashbrown::HashMap; +use trash::{TrashItem, os_limited}; +use yazi_macro::ok_or_not_found; +use yazi_shim::Twox128; + +use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::{Cha, ChaSig}, file::File}; + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { Ok(Self) } + + pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { + let Some(node) = node else { + return os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .map(|item| TrashEntry::new(Self::path(&item.id)?, item.name, item.id)) + .collect(); + }; + + fs::read_dir(self.resolve(node)?)? + .map(|entry| { + let entry = entry?; + let name = entry.file_name(); + let path = entry.path(); + TrashEntry::new(path, name.clone(), name) + }) + .collect() + } + + pub fn entry(&self, node: &TrashNode) -> io::Result { + let path = self.resolve(node)?; + let name = path + .file_name() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))? + .to_owned(); + TrashEntry::new(path, name, node.key.clone()) + } + + pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result { + let path = self.resolve(node)?; + if let Some(name) = node.rel.file_name() { + Cha::from_trash(&path, name, follow) + } else { + let item = self.top_item(&node.top)?; + Cha::from_trash(&path, &item.name, follow) + } + } + + pub(super) fn revalidate( + &self, + node: Option<&TrashNode>, + current: &File, + ) -> io::Result> { + let latest = if let Some(node) = node { + self.entry(node)?.into_file(¤t.url) + } else { + let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect(); + roots.sort_unstable(); + + let mut h = Twox128::default(); + for root in roots { + let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue); + let cha = Cha::new(root.file_name().unwrap_or_default(), meta); + + root.hash(&mut h); + ChaSig(cha).hash(&mut h); + } + + let hash = h.finish_128(); + File { + cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) }, + ..current.clone() + } + }; + + let changed = !latest.cha.hits(current.cha) + || latest.extra.link_to() != current.extra.link_to() + || latest.extra.backing() != current.extra.backing(); + + Ok(changed.then_some(latest)) + } + + pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { + if node.rel.as_os_str().is_empty() { + os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) + } else { + fs::remove_file(self.resolve(node)?) + } + } + + pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { + if node.rel.as_os_str().is_empty() { + os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) + } else { + fs::remove_dir(self.resolve(node)?) + } + } + + pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { + let mut tops = Vec::new(); + let items: HashMap<_, _> = os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .map(|item| (item.id.clone(), item)) + .collect(); + + for node in nodes { + let item = items + .get(&node.top) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?; + if node.rel.as_os_str().is_empty() { + tops.push(item.clone()); + continue; + } + + let from = self.resolve(&node)?; + let to = item.original_path().join(node.rel); + + let is_dir = fs::symlink_metadata(&from)?.is_dir(); + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + + match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } { + Ok(()) => fs::rename(from, to), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )), + Err(e) => Err(e), + }?; + } + + os_limited::restore_all(tops).map_err(io::Error::other) + } + + pub fn empty(&self) -> io::Result<()> { + os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other) + } + + fn resolve(&self, node: &TrashNode) -> io::Result { + let path = Self::path(&node.top)?; + Ok(if node.rel.as_os_str().is_empty() { path } else { path.join(&node.rel) }) + } + + fn top_item(&self, key: &OsStr) -> io::Result { + os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .find(|item| item.id == key) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists")) + } + + fn path(key: &OsStr) -> io::Result { + let info = Path::new(key); // Path of the trash info file, e.g. "~/.local/share/Trash/info/filename.txt.trashinfo" + let parent = info + .parent() + .and_then(|parent| parent.parent()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?; + let stem = info + .file_stem() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?; + + Ok(parent.join("files").join(stem)) + } +} diff --git a/yazi-fs/src/trash/lua.rs b/yazi-fs/src/trash/lua.rs new file mode 100644 index 00000000..36058e63 --- /dev/null +++ b/yazi-fs/src/trash/lua.rs @@ -0,0 +1,73 @@ +use std::io; + +use mlua::{ExternalError, ExternalResult, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; +use tokio::task::spawn_blocking; +use yazi_binding::Error; + +use super::{Trash, TrashNode, TrashNodes}; +use crate::file::File; + +impl UserData for Trash { + fn add_methods>(methods: &mut M) { + methods.add_async_function("empty", |lua, ()| async move { + match spawn_blocking(|| Trash::new()?.empty()).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("list", |lua, node: Option| async move { + match spawn_blocking(move || Trash::new()?.list(node.as_ref())).await.into_lua_err()? { + Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("entry", |lua, node: TrashNode| async move { + match spawn_blocking(move || Trash::new()?.entry(&node)).await.into_lua_err()? { + Ok(entry) => entry.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("metadata", |lua, (node, follow): (TrashNode, bool)| async move { + match spawn_blocking(move || Trash::new()?.metadata(&node, follow)).await.into_lua_err()? { + Ok(cha) => cha.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function( + "revalidate", + |lua, (node, file): (Option, File)| async move { + match spawn_blocking(move || Trash::new()?.revalidate(node.as_ref(), &file)) + .await + .into_lua_err()? + { + Ok(file) => file.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }, + ); + + methods.add_async_function("remove", |lua, (kind, node): (LuaString, TrashNode)| async move { + let f: fn(&Trash, &TrashNode) -> io::Result<()> = match &*kind.as_bytes() { + b"file" => Trash::remove_file, + b"dir" => Trash::remove_dir, + _ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?, + }; + + match spawn_blocking(move || f(&Trash::new()?, &node)).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("restore", |lua, nodes: TrashNodes| async move { + match spawn_blocking(move || Trash::new()?.restore(nodes)).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + } +} diff --git a/yazi-fs/src/trash/macos.rs b/yazi-fs/src/trash/macos.rs new file mode 100644 index 00000000..b1a04cf5 --- /dev/null +++ b/yazi-fs/src/trash/macos.rs @@ -0,0 +1,188 @@ +use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}}; + +use ds_parser::Value; +use hashbrown::HashMap; +use yazi_macro::ok_or_not_found; + +use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::Cha, file::File}; + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { Ok(Self) } + + pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { + let it = match fs::read_dir(self.resolve(node)?) { + Ok(it) => it, + Err(e) if e.kind() == io::ErrorKind::NotFound && node.is_none() => return Ok(vec![]), + Err(e) => return Err(e), + }; + + it.map(|entry| { + let entry = entry?; + let name = entry.file_name(); + let path = entry.path(); + let key = if node.is_some() { name.clone() } else { path.as_os_str().to_owned() }; + TrashEntry::new(path, name, key) + }) + .collect() + } + + pub fn entry(&self, node: &TrashNode) -> io::Result { + let path = self.resolve(Some(node))?; + let name = path + .file_name() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))? + .to_owned(); + TrashEntry::new(path, name, node.key.clone()) + } + + pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result { + let path = self.resolve(Some(node))?; + Cha::from_trash(&path, path.file_name().unwrap_or_default(), follow) + } + + pub(super) fn revalidate( + &self, + node: Option<&TrashNode>, + current: &File, + ) -> io::Result> { + let latest = if let Some(node) = node { + self.entry(node)?.into_file(¤t.url) + } else { + let path = self.resolve(None)?; + let cha = match fs::symlink_metadata(&path) { + Ok(meta) => Cha::new(path.file_name().unwrap_or_default(), meta), + Err(e) if e.kind() == io::ErrorKind::NotFound => Cha::from_mold(true), + Err(e) => return Err(e), + }; + File { cha, ..current.clone() } + }; + + let changed = !latest.cha.hits(current.cha) + || latest.extra.link_to() != current.extra.link_to() + || latest.extra.backing() != current.extra.backing(); + + Ok(changed.then_some(latest)) + } + + pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { + fs::remove_file(self.resolve(Some(node))?) + } + + pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { + fs::remove_dir(self.resolve(Some(node))?) + } + + pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { + let root = self.resolve(None)?; + let locations = OriginalLocation::parse(&fs::read(root.join(".DS_Store"))?)?; + + for node in nodes { + let name = Path::new(&node.top).file_name().unwrap_or_default(); + let location = locations.get(name).ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("original location is unavailable for trash item: {:?}", node.top), + ) + })?; + + let from = self.resolve(Some(&node))?; + let to = location.join(&node)?; + + let is_dir = fs::symlink_metadata(&from)?.is_dir(); + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + + match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } { + Ok(()) => fs::rename(from, to), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )), + Err(e) => Err(e), + }?; + } + + Ok(()) + } + + pub fn empty(&self) -> io::Result<()> { + let root = self.resolve(None)?; + for entry in ok_or_not_found!(fs::read_dir(root), return Ok(())) { + let entry = entry?; + if entry.file_type()?.is_dir() { + fs::remove_dir_all(entry.path())?; + } else { + fs::remove_file(entry.path())?; + } + } + Ok(()) + } + + fn resolve(&self, node: Option<&TrashNode>) -> io::Result { + if let Some(node) = node { + let top = Path::new(&node.top); + return Ok(if node.rel.as_os_str().is_empty() { top.into() } else { top.join(&node.rel) }); + } + + Ok( + dirs::home_dir() + .filter(|p| p.is_absolute()) + .ok_or_else(|| { + io::Error::other("cannot determine home directory for trash root resolution") + })? + .join(".Trash"), + ) + } +} + +// --- OriginalLocation +#[derive(Default)] +struct OriginalLocation { + parent: Option, + name: Option, +} + +impl OriginalLocation { + fn parse(bytes: &[u8]) -> io::Result> { + let store = ds_parser::parse(bytes).map_err(io::Error::other)?; + let mut locations = HashMap::::new(); + + for record in store.records { + let Value::Ustr(value) = record.value else { continue }; + if value.is_empty() { + continue; + } + + let location = locations.entry_ref(OsStr::new(&record.name)).or_default(); + match &record.field.fourcc().bytes() { + b"ptbL" => location.parent = Some(value.into()), + b"ptbN" => location.name = Some(value.into()), + _ => {} + } + } + + Ok(locations) + } + + fn join(&self, node: &TrashNode) -> io::Result { + let parent = self.parent.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back location") + })?; + + let name = self.name.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back name") + })?; + + let mut components = Path::new(name).components(); + if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash put-back name")); + } + + let top_path = Path::new("/").join(parent).join(name); + Ok(if node.rel.as_os_str().is_empty() { top_path } else { top_path.join(&node.rel) }) + } +} diff --git a/yazi-fs/src/trash/mod.rs b/yazi-fs/src/trash/mod.rs new file mode 100644 index 00000000..e48f2913 --- /dev/null +++ b/yazi-fs/src/trash/mod.rs @@ -0,0 +1,16 @@ +yazi_macro::mod_flat!(entry lua node nodes); + +#[cfg(target_os = "macos")] +yazi_macro::mod_flat!(macos); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); + +#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))] +yazi_macro::mod_flat!(freedesktop); + +#[cfg(any(target_os = "android", target_os = "ios"))] +yazi_macro::mod_flat!(unsupported); + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +yazi_macro::mod_flat!(traits); diff --git a/yazi-fs/src/trash/node.rs b/yazi-fs/src/trash/node.rs new file mode 100644 index 00000000..dc97c26f --- /dev/null +++ b/yazi-fs/src/trash/node.rs @@ -0,0 +1,49 @@ +use std::{ffi::OsString, io, path::{Component, PathBuf}}; + +use mlua::{BorrowedBytes, FromLua, IntoLua, Lua, Table, Value}; +use yazi_shared::{path::PathBufDyn, strand::AsStrand}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TrashNode { + pub(super) key: OsString, + pub(super) top: OsString, + pub(super) rel: PathBuf, +} + +impl TrashNode { + pub(super) fn validate(&self) -> io::Result<()> { + if self.key.is_empty() || self.top.is_empty() || !self.rel.is_relative() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node")); + } + if self.rel.components().any(|c| matches!(c, Component::ParentDir)) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node path")); + } + Ok(()) + } +} + +impl FromLua for TrashNode { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let t = Table::from_lua(value, lua)?; + let node = Self { + key: t.raw_get::("key")?.as_strand().to_os_string()?, + top: t.raw_get::("top")?.as_strand().to_os_string()?, + rel: t.raw_get::("rel")?.into_os()?, + }; + + node.validate().map_err(mlua::Error::external)?; + Ok(node) + } +} + +impl IntoLua for TrashNode { + fn into_lua(self, lua: &Lua) -> mlua::Result { + lua + .create_table_from([ + ("key", lua.create_external_string(self.key.into_encoded_bytes())?.into_lua(lua)?), + ("top", lua.create_external_string(self.top.into_encoded_bytes())?.into_lua(lua)?), + ("rel", PathBufDyn::from(self.rel).into_lua(lua)?), + ])? + .into_lua(lua) + } +} diff --git a/yazi-fs/src/trash/nodes.rs b/yazi-fs/src/trash/nodes.rs new file mode 100644 index 00000000..d6521780 --- /dev/null +++ b/yazi-fs/src/trash/nodes.rs @@ -0,0 +1,36 @@ +use std::vec; + +use mlua::{FromLua, Lua, Value}; + +use super::TrashNode; + +pub struct TrashNodes(Vec); + +impl TrashNodes { + fn new(mut nodes: Vec) -> Self { + nodes.sort_unstable_by_key(|node| node.rel.components().count()); + + let mut seen = Vec::::with_capacity(nodes.len()); + for node in nodes { + if seen.iter().any(|parent| parent.top == node.top && node.rel.starts_with(&parent.rel)) { + continue; + } + seen.push(node); + } + Self(seen) + } +} + +impl FromLua for TrashNodes { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let nodes = Vec::::from_lua(value, lua)?; + Ok(Self::new(nodes)) + } +} + +impl IntoIterator for TrashNodes { + type IntoIter = vec::IntoIter; + type Item = TrashNode; + + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} diff --git a/yazi-fs/src/trash/traits.rs b/yazi-fs/src/trash/traits.rs new file mode 100644 index 00000000..2c84c766 --- /dev/null +++ b/yazi-fs/src/trash/traits.rs @@ -0,0 +1,36 @@ +use crate::cha::{Cha, ChaKind, ChaMode}; + +pub(super) trait TrashCha: Sized { + fn from_mold(is_dir: bool) -> Self; + + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + fn from_trash( + path: &std::path::Path, + name: &std::ffi::OsStr, + follow: bool, + ) -> std::io::Result; +} + +impl TrashCha for Cha { + fn from_mold(is_dir: bool) -> Self { + let mut cha = Self::default(); + cha.kind.remove(ChaKind::DUMMY); + cha.mode = if is_dir { ChaMode::T_DIR | ChaMode::U_EXEC } else { ChaMode::T_FILE }; + cha.mode |= ChaMode::U_READ | ChaMode::U_WRITE; + cha + } + + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + fn from_trash( + path: &std::path::Path, + name: &std::ffi::OsStr, + follow: bool, + ) -> std::io::Result { + let cha = Cha::new(name, std::fs::symlink_metadata(path)?); + Ok(if cha.is_link() && follow { + cha.follow(std::fs::metadata(path).ok().map(|meta| Cha::new(name, meta))) + } else { + cha + }) + } +} diff --git a/yazi-fs/src/trash/unsupported.rs b/yazi-fs/src/trash/unsupported.rs new file mode 100644 index 00000000..464e4026 --- /dev/null +++ b/yazi-fs/src/trash/unsupported.rs @@ -0,0 +1,46 @@ +use std::io; + +use super::{TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::Cha, file::File}; + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { Ok(Self) } + + pub fn list(&self, _node: Option<&TrashNode>) -> io::Result> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn entry(&self, _node: &TrashNode) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn metadata(&self, _node: &TrashNode, _: bool) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub(super) fn revalidate( + &self, + _node: Option<&TrashNode>, + _current: &File, + ) -> io::Result> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn remove_file(&self, _node: &TrashNode) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn remove_dir(&self, _node: &TrashNode) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn restore(&self, _nodes: TrashNodes) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn empty(&self) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } +} diff --git a/yazi-fs/src/trash/windows.rs b/yazi-fs/src/trash/windows.rs new file mode 100644 index 00000000..167f068d --- /dev/null +++ b/yazi-fs/src/trash/windows.rs @@ -0,0 +1,330 @@ +use std::{ffi::{OsStr, OsString, c_void}, fs, hash::Hash, io, mem, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}}; + +use hashbrown::HashMap; +use trash::{TrashItem, os_limited}; +use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::*, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}}; +use yazi_ffi::Com; +use yazi_shim::Twox128; + +use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::Cha, file::File}; + +thread_local! { + static COM: io::Result = Com::new(); +} + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { + COM.with(|result| { + result.as_ref().map(|_| Self).map_err(|e| io::Error::new(e.kind(), e.to_string())) + }) + } + + pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { + let Some(node) = node else { + return os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .map(|item| ShellItem::new(&item.id)?.entry(item.name, item.id)) + .collect(); + }; + + self + .resolve(node)? + .children()? + .into_iter() + .map(|item| { + let name = item.display_name(SIGDN_PARENTRELATIVE)?; + item.entry(name.clone(), name) + }) + .collect() + } + + pub fn entry(&self, node: &TrashNode) -> io::Result { + let item = self.resolve(node)?; + item.entry(item.display_name(SIGDN_PARENTRELATIVE)?, &node.key) + } + + pub fn metadata(&self, node: &TrashNode, _: bool) -> io::Result { self.resolve(node)?.cha() } + + pub(super) fn revalidate( + &self, + node: Option<&TrashNode>, + current: &File, + ) -> io::Result> { + let cha = + if let Some(node) = node { TrashSig::item(&self.resolve(node)?)? } else { TrashSig::root()? }; + + Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) }) + } + + pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { + if node.rel.as_os_str().is_empty() { + os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) + } else { + self.resolve(node)?.delete() + } + } + + pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { + if node.rel.as_os_str().is_empty() { + return os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other); + } + + let item = self.resolve(node)?; + if !item.children()?.is_empty() { + return Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty")); + } + item.delete() + } + + pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { + let mut tops = Vec::new(); + let items: HashMap<_, _> = os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .map(|item| (item.id.clone(), item)) + .collect(); + + for node in nodes { + let item = items + .get(&node.top) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?; + if node.rel.as_os_str().is_empty() { + tops.push(item.clone()); + } else { + let to = item.original_path().join(&node.rel); + self.restore_do(&self.resolve(&node)?, &to)?; + } + } + os_limited::restore_all(tops).map_err(io::Error::other) + } + + fn restore_do(&self, item: &ShellItem, to: &Path) -> io::Result<()> { + match fs::symlink_metadata(to) { + Ok(_) => { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )); + } + Err(e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + + // Create parent directories + let parent = to + .parent() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?; + fs::create_dir_all(parent)?; + + let parent = ShellItem::new(parent.as_os_str())?; + let name: Vec = to + .file_name() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))? + .encode_wide() + .chain([0]) + .collect(); + + operate(FOF_NO_UI | FOFX_EARLYFAILURE, |operation| unsafe { + operation.MoveItem(&item.0, &parent.0, PCWSTR(name.as_ptr()), None) + }) + } + + pub fn empty(&self) -> io::Result<()> { + os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other) + } + + fn resolve(&self, node: &TrashNode) -> io::Result { + let top = ShellItem::new(&node.top)?; + if node.rel.as_os_str().is_empty() { + return Ok(top); + } + + let path = PathBuf::from(top.display_name(SIGDN_FILESYSPATH)?).join(&node.rel); + ShellItem::new(path.as_os_str()) + } + + fn top_item(&self, key: &OsStr) -> io::Result { + os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .find(|item| item.id == key) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists")) + } +} + +// --- TrashSig +struct TrashSig { + names: Option>, + count_size: Option<(i64, i64)>, +} + +impl TrashSig { + fn root() -> io::Result { + let root = ShellItem::root()?; + let cha = root.cha()?; + + let mut info = + SHQUERYRBINFO { cbSize: mem::size_of::() as u32, ..Default::default() }; + + let sig = if unsafe { SHQueryRecycleBinW(PCWSTR::null(), &mut info) }.is_ok() { + Self { names: None, count_size: Some((info.i64NumItems, info.i64Size)) } + } else if cha.mtime.is_some() { + Self { names: None, count_size: None } + } else { + Self { names: Some(Self::names(&root)?), count_size: None } + }; + + Ok(sig.into_cha(cha)) + } + + fn item(item: &ShellItem) -> io::Result { + let cha = item.cha()?; + let sig = if cha.mtime.is_none() && cha.is_dir() { + Self { names: Some(Self::names(item)?), count_size: None } + } else { + Self { names: None, count_size: None } + }; + + Ok(sig.into_cha(cha)) + } + + fn names(item: &ShellItem) -> io::Result> { + let mut names: Vec<_> = item + .children()? + .into_iter() + .map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)) + .collect::>()?; + names.sort_unstable(); + Ok(names) + } + + fn into_cha(self, mut cha: Cha) -> Cha { + let mut h = Twox128::default(); + if let Some((count, size)) = self.count_size { + (size, count).hash(&mut h); + } else if let Some(names) = self.names { + names.hash(&mut h); + } else { + return cha; + } + + let hash = h.finish_128(); + cha.ctime = UNIX_EPOCH.checked_add(Duration::from_nanos(hash as u64 ^ (hash >> 64) as u64)); + cha + } +} + +// --- ShellItem +struct ShellItem(IShellItem); + +impl ShellItem { + fn new(name: &OsStr) -> io::Result { + let name: Vec = name.encode_wide().chain([0]).collect(); + unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error) + } + + fn root() -> io::Result { + unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) } + .map(Self) + .map_err(error) + } + + fn children(&self) -> io::Result> { + let items: IEnumShellItems = + unsafe { self.0.BindToHandler(None, &BHID_EnumItems).map_err(error)? }; + + let mut result = Vec::new(); + loop { + let mut fetched = 0; + let mut next = [None]; + unsafe { items.Next(&mut next, Some(&mut fetched)).map_err(error)? }; + if fetched == 0 { + break; + } else if let Some(item) = next[0].take() { + result.push(Self(item)); + } + } + Ok(result) + } + + fn cha(&self) -> io::Result { + if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) { + match fs::metadata(&backing) { + Ok(meta) => return Ok(Cha::new(backing.file_name().unwrap_or_default(), meta)), + Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(e), + Err(_) => {} + } + } + + let item: IShellItem2 = self.0.cast().map_err(error)?; + let is_dir = unsafe { self.0.GetAttributes(SFGAO_FOLDER) }.map_err(error)? == SFGAO_FOLDER; + + let mut cha = Cha::from_mold(is_dir); + cha.len = unsafe { item.GetUInt64(&PKEY_Size) }.unwrap_or_default(); + cha.mtime = unsafe { item.GetFileTime(&PKEY_DateModified) }.ok().and_then(system_time); + Ok(cha) + } + + fn entry(&self, name: N, key: K) -> io::Result + where + N: Into, + K: Into, + { + Ok(TrashEntry { + name: name.into(), + key: key.into(), + cha: self.cha()?, + link_to: None, + backing: self.display_name(SIGDN_FILESYSPATH)?.into(), + }) + } + + fn delete(&self) -> io::Result<()> { + operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) }) + } + + fn display_name(&self, kind: SIGDN) -> io::Result { + unsafe { + let name = self.0.GetDisplayName(kind).map_err(error)?; + let result = OsString::from_wide(name.as_wide()); + CoTaskMemFree(Some(name.0.cast::())); + Ok(result) + } + } +} + +fn operate(flags: FILEOPERATION_FLAGS, f: F) -> io::Result<()> +where + F: FnOnce(&IFileOperation) -> windows::core::Result<()>, +{ + let aborted = unsafe { + let operation: IFileOperation = + CoCreateInstance(&FileOperation, None, CLSCTX_ALL).map_err(error)?; + operation.SetOperationFlags(flags).map_err(error)?; + f(&operation).map_err(error)?; + operation.PerformOperations().map_err(error)?; + operation.GetAnyOperationsAborted().map_err(error)?.as_bool() + }; + + if aborted { Err(io::Error::other("trash operation was aborted")) } else { Ok(()) } +} + +fn error(error: windows::core::Error) -> io::Error { + if error.code() == ERROR_FILE_NOT_FOUND.to_hresult() + || error.code() == ERROR_PATH_NOT_FOUND.to_hresult() + { + io::Error::new(io::ErrorKind::NotFound, error) + } else { + io::Error::other(error) + } +} + +fn system_time(time: FILETIME) -> Option { + let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime); + let ticks = ticks.checked_sub(116_444_736_000_000_000)?; + Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100)) +} diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index effbf782..88fde208 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,88 +1,84 @@ -use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; +use std::{borrow::Cow, path::{Path, PathBuf}}; use mlua::UserDataFields; -use yazi_shared::{path::{DynPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; +use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}; use yazi_shim::mlua::UserDataFieldsExt; -use crate::{FsHash128, FsSpec, path::PercentEncoding}; +use crate::{FsAuth, FsHash128}; pub trait FsUrl<'a> { - fn cache(&self) -> Option; + fn cache_bucket(&self) -> Option; - fn cache_lock(&self) -> Option; + fn cache_entry(&self) -> Option; - fn unified_path(self) -> Cow<'a, Path>; + fn stamp_entry(&self) -> Option; - fn unified_path_str(self) -> Cow<'a, OsStr> - where - Self: Sized, - { - match self.unified_path() { - Cow::Borrowed(p) => p.as_os_str().into(), - Cow::Owned(p) => p.into_os_string().into(), - } - } + fn working_path(self) -> Cow<'a, Path>; } impl<'a> FsUrl<'a> for Url<'a> { - fn cache(&self) -> Option { - fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { - let mut it = loc.components(); - if it.next() == Some(yazi_shared::path::Component::RootDir) { - root.push(it.dyn_path().percent_encode()); - } else { - root.push(".%2F"); - root.push(loc.percent_encode()); - } - root - } - - self.auth().cache().map(|root| with_loc(self.loc(), root)) - } - - fn cache_lock(&self) -> Option { - self.auth().cache().map(|mut root| { - root.push("%lock"); - root.push(format!("{:x}", self.hash_u128())); + fn cache_bucket(&self) -> Option { + self.auth().cache_root().map(|mut root| { + root.push(self.hash_u128_str(&mut [0; 26])); root }) } - fn unified_path(self) -> Cow<'a, Path> { + fn cache_entry(&self) -> Option { + let parent = self.parent()?; + parent.auth().cache_root().map(|mut root| { + root.push(parent.hash_u128_str(&mut [0; 26])); + root.push(self.hash_u128_str(&mut [0; 26])); + root + }) + } + + fn stamp_entry(&self) -> Option { + self.auth().stamp_root().map(|mut root| { + root.push(self.hash_u128_str(&mut [0; 26])); + root + }) + } + + fn working_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { - self.cache().expect("non-local URL should have a cache path").into() + self.cache_bucket().expect("non-local URL should have a cache path").into() } } } } impl FsUrl<'_> for UrlBuf { - fn cache(&self) -> Option { self.as_url().cache() } + fn cache_bucket(&self) -> Option { self.as_url().cache_bucket() } - fn cache_lock(&self) -> Option { self.as_url().cache_lock() } + fn cache_entry(&self) -> Option { self.as_url().cache_entry() } - fn unified_path(self) -> Cow<'static, Path> { + fn stamp_entry(&self) -> Option { self.as_url().stamp_entry() } + + fn working_path(self) -> Cow<'static, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { - self.cache().expect("non-local URL should have a cache path").into() + self.cache_bucket().expect("non-local URL should have a cache path").into() } } } } impl<'a> FsUrl<'a> for UrlCow<'a> { - fn cache(&self) -> Option { self.as_url().cache() } + fn cache_bucket(&self) -> Option { self.as_url().cache_bucket() } - fn cache_lock(&self) -> Option { self.as_url().cache_lock() } + fn cache_entry(&self) -> Option { self.as_url().cache_entry() } - fn unified_path(self) -> Cow<'a, Path> { + fn stamp_entry(&self) -> Option { self.as_url().stamp_entry() } + + fn working_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(), Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { - self.cache().expect("non-local URL should have a cache path").into() + self.cache_bucket().expect("non-local URL should have a cache path").into() } } } @@ -92,7 +88,6 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { inventory::submit! { UrlBufInventory { register: |registry| { - registry.add_cached_field("cache", |_, me| Ok(me.cache())); registry.add_cached_field("domain", |lua, me| { yazi_binding::deprecate!(lua, "{}: `Url.domain` is deprecated, use `Url.spec.domain` instead."); lua.create_string(&*me.spec().domain) diff --git a/yazi-macro/src/event.rs b/yazi-macro/src/event.rs index f4f9253a..1e9c8db7 100644 --- a/yazi-macro/src/event.rs +++ b/yazi-macro/src/event.rs @@ -1,13 +1,13 @@ #[macro_export] macro_rules! emit { (Call($action:expr)) => { - yazi_shared::event::Event::Call(yazi_shared::event::ActionCow::from($action)).emit(); + yazi_shared::event::Event::Call(yazi_shared::event::ActionCow::from($action)).emit() }; (Seq($actions:expr)) => { - yazi_shared::event::Event::Seq($actions).emit(); + yazi_shared::event::Event::Seq($actions).emit() }; ($event:ident) => { - yazi_shared::event::Event::$event.emit(); + yazi_shared::event::Event::$event.emit() }; } diff --git a/yazi-macro/src/render.rs b/yazi-macro/src/render.rs index 38c075f2..798bed8e 100644 --- a/yazi-macro/src/render.rs +++ b/yazi-macro/src/render.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! render { () => { - yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed); + yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed) }; ($cond:expr) => { if $cond { diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index 8788a887..4df54db3 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -317,7 +317,7 @@ function M.treelize(items, tops, parents) end local buf, it = {}, f.path.parent - while it and it ~= tops[#tops] do + while it and #it ~= 0 and it ~= tops[#tops] do buf[#buf + 1], it = it, it.parent end for i = #buf, 1, -1 do diff --git a/yazi-plugin/preset/plugins/image.lua b/yazi-plugin/preset/plugins/image.lua index 7e3315ac..1cfaf494 100644 --- a/yazi-plugin/preset/plugins/image.lua +++ b/yazi-plugin/preset/plugins/image.lua @@ -3,7 +3,7 @@ local M = {} function M:peek(job) local start, url = os.clock(), ya.file_cache(job) if not url or not fs.cha(url) then - url = job.file.url + url = Url(job.file.path) end ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock())) @@ -20,7 +20,7 @@ function M:preload(job) return true end - return ya.image_precache(job.file.url, cache) + return ya.image_precache(Url(job.file.path), cache) end function M:spot(job) @@ -41,7 +41,7 @@ function M:spot(job) end function M:spot_base(job) - local info = ya.image_info(job.file.url) + local info = ya.image_info(Url(job.file.path)) if not info then return {} end diff --git a/yazi-plugin/preset/plugins/mime-local.lua b/yazi-plugin/preset/plugins/mime-local.lua index 9f4bb42b..fc9a26aa 100644 --- a/yazi-plugin/preset/plugins/mime-local.lua +++ b/yazi-plugin/preset/plugins/mime-local.lua @@ -6,11 +6,7 @@ local M = {} function M:fetch(job) local urls, paths = {}, {} for i, file in ipairs(job.files) do - if file.cache then - urls[i], paths[i] = tostring(file.url), tostring(file.cache) - else - paths[i] = tostring(file.path) - end + urls[i], paths[i] = file.url, tostring(file.path) end local child, err = M.spawn_file1(paths) @@ -42,7 +38,7 @@ function M:fetch(job) match, ignore = M.match_mimetype(line) if match then - updates[urls[i] or paths[i]], state[i], i = match, true, i + 1 + updates[urls[i]], state[i], i = match, true, i + 1 flush(false) elseif not ignore then state[i], i = false, i + 1 @@ -103,7 +99,7 @@ function M.placeholder(err, urls, paths) local updates = {} for i = 1, #paths do - updates[urls[i] or paths[i]] = "null/file1-not-found" + updates[urls[i]] = "null/file1-not-found" end ya.emit("update_mimes", { updates = updates }) diff --git a/yazi-plugin/preset/plugins/mime-remote.lua b/yazi-plugin/preset/plugins/mime-remote.lua index 2bd474d8..d8dd8846 100644 --- a/yazi-plugin/preset/plugins/mime-remote.lua +++ b/yazi-plugin/preset/plugins/mime-remote.lua @@ -2,16 +2,16 @@ local M = {} local function stale_cache(file) local url = file.url - local lock = url.spec.cache:join(string.format("%%lock/%s", url:hash(true))) + local stamp = url.spec.stamp:join(url:hash(true)) - local fd = fs.access():read(true):open(Url(lock)) + local fd = fs.access():read(true):open(Url(stamp)) if not fd then return true end - local hash = fd:read(32) + local sig = fd:read(26) ya.drop(fd) - return hash ~= file.cha:hash(true) + return sig ~= file.cha:hash(true) end function M:fetch(job) diff --git a/yazi-plugin/preset/plugins/trash.lua b/yazi-plugin/preset/plugins/trash.lua new file mode 100644 index 00000000..79c3e4a8 --- /dev/null +++ b/yazi-plugin/preset/plugins/trash.lua @@ -0,0 +1,153 @@ +local M = {} + +local function top(url) + while url.parent and url.parent.name do + url = url.parent + end + return url.name and url or nil +end + +local function node(url) + local top = top(url) + if not top then + return + end + return { + key = url.spec.domain, + top = top.spec.domain, + rel = url:strip_prefix(top) or Path.os(""), + } +end + +local function absolute(url) + if url.is_absolute then + return fs.clean_url(url) + end + + local cwd, err = fs.cwd() + if not cwd then + return nil, err + end + + local root = cwd.path + while root.parent do + root = root.parent + end + return fs.clean_url(url:join(root:join(url.path))) +end + +local function file(url, entry) + entry.url = url + return File(entry) +end + +local function files(parent, entries) + for i, entry in ipairs(entries) do + local url = parent:join(Path.os(entry.name)):into_domain(entry.key) + entries[i] = file(url, entry) + end + return entries +end + +local function notify(action, err) + ya.notify { + title = "Trash", + content = string.format("Failed to %s: %s", action, err), + level = "error", + timeout = 10, + } +end + +function M:setup() + ps.sub_remote("trash-restore", function(args) + ya.async(function() + local nodes = {} + for i, arg in ipairs(args) do + nodes[i] = node(Url(arg)) + if not nodes[i] then + return notify("restore", "Cannot restore the trash root") + end + end + + local ok, err = fs.trash.restore(nodes) + if ok then + ya.emit("refresh", {}) + else + notify("restore", err) + end + end) + end) + + ps.sub_remote("trash-empty", function() + ya.async(function() + local confirmed = ya.confirm { + pos = { "center", w = 60, h = 10 }, + title = ui.Line("Empty trash?"):style(th.confirm.title), + body = ui.Text("All items in the trash will be permanently deleted."), + } + if not confirmed then + return + end + + local ok, err = fs.trash.empty() + if ok then + ya.emit("refresh", {}) + else + notify("empty", err) + end + end) + end) +end + +function M:entry() + local url, err = absolute(Url("trash:///@/.")) + if url then + ya.emit("cd", { url }) + else + notify("open", err) + end +end + +function M:provide(job) + local op = job.op + if op == "Absolute" or op == "Canonicalize" then + return absolute(job.url) + elseif op == "Casefold" then + return job.url + elseif op == "Metadata" or op == "SymlinkMetadata" then + local n = node(job.url) + if n then + return fs.trash.metadata(n, op == "Metadata") + else + return Cha { mode = tonumber("40700", 8) } + end + elseif op == "ReadDir" then + local entries, err = fs.trash.list(node(job.url)) + if entries then + return files(job.url, entries) + else + return nil, err + end + elseif op == "Revalidate" then + return fs.trash.revalidate(node(job.file.url), job.file) + elseif op == "File" then + local n = node(job.url) + if not n then + return nil, Error.custom("Cannot construct a file for the trash root") + end + local entry, err = fs.trash.entry(n) + if entry then + return file(job.url, entry) + else + return nil, err + end + elseif op == "RemoveFile" then + return fs.trash.remove("file", node(job.url)) + elseif op == "RemoveDir" then + return fs.trash.remove("dir", node(job.url)) + end + + return false, Error.custom("Unsupported trash operation: " .. op) +end + +return M diff --git a/yazi-plugin/preset/setup.lua b/yazi-plugin/preset/setup.lua index 10a6a343..30213906 100644 --- a/yazi-plugin/preset/setup.lua +++ b/yazi-plugin/preset/setup.lua @@ -2,3 +2,4 @@ os.setlocale("") require("dds"):setup() require("extract"):setup() +require("trash"):setup() diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index aa245d6f..6a022481 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::{Child, Command}, sync::mpsc::{self, UnboundedReceiver}}; use yazi_fs::{FsUrl, file::File}; use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; pub struct FdOpt { pub cwd: UrlBuf, @@ -27,7 +27,7 @@ pub fn fd(opt: FdOpt) -> Result> { let Ok(url) = opt.cwd.try_join(line) else { continue; }; - if let Ok(file) = File::new(url).await { + if let Ok(file) = engine::file(url).await { tx.send(file).ok(); } } @@ -39,7 +39,7 @@ pub fn fd(opt: FdOpt) -> Result> { fn spawn(program: &str, opt: &FdOpt) -> std::io::Result { Command::new(program) .arg("--base-directory") - .arg(&*opt.cwd.as_url().unified_path()) + .arg(&*opt.cwd.as_url().working_path()) .arg("--regex") .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(&opt.args) diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index b6abf320..0d917f13 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; use yazi_fs::{FsUrl, file::File}; use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; pub struct RgOpt { pub cwd: UrlBuf, @@ -19,7 +19,7 @@ pub fn rg(opt: RgOpt) -> Result> { .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .current_dir(&*opt.cwd.as_url().unified_path()) + .current_dir(opt.cwd.as_url().working_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -36,7 +36,7 @@ pub fn rg(opt: RgOpt) -> Result> { let Ok(url) = opt.cwd.try_join(line) else { continue; }; - if let Ok(file) = File::new(url).await { + if let Ok(file) = engine::file(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/external/rga.rs b/yazi-plugin/src/external/rga.rs index bfece528..a31c35e1 100644 --- a/yazi-plugin/src/external/rga.rs +++ b/yazi-plugin/src/external/rga.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; use yazi_fs::{FsUrl, file::File}; use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; pub struct RgaOpt { pub cwd: UrlBuf, @@ -19,7 +19,7 @@ pub fn rga(opt: RgaOpt) -> Result> { .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .current_dir(&*opt.cwd.as_url().unified_path()) + .current_dir(opt.cwd.as_url().working_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -36,7 +36,7 @@ pub fn rga(opt: RgaOpt) -> Result> { let Ok(url) = opt.cwd.try_join(line) else { continue; }; - if let Ok(file) = File::new(url).await { + if let Ok(file) = engine::file(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/fs/calculator.rs b/yazi-plugin/src/fs/calculator.rs index 294c01ab..949b34df 100644 --- a/yazi-plugin/src/fs/calculator.rs +++ b/yazi-plugin/src/fs/calculator.rs @@ -3,7 +3,7 @@ use yazi_binding::Error; pub enum SizeCalculator { Local(yazi_fs::engine::local::SizeCalculator), - Remote(yazi_vfs::engine::SizeCalculator), + Virtual(yazi_vfs::engine::SizeCalculator), } impl UserData for SizeCalculator { @@ -11,7 +11,7 @@ impl UserData for SizeCalculator { fields.add_field_method_get("cha", |_, me| { Ok(match me { Self::Local(c) => c.cha(), - Self::Remote(c) => c.cha(), + Self::Virtual(c) => c.cha(), }) }); } @@ -20,7 +20,7 @@ impl UserData for SizeCalculator { methods.add_async_method_mut("recv", |lua, mut me, ()| async move { let next = match &mut *me { Self::Local(c) => c.next().await, - Self::Remote(c) => c.next().await, + Self::Virtual(c) => c.next().await, }; match next { diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 5f6ca642..16da9a2b 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -5,7 +5,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet, Error}; use yazi_config::Pattern; use yazi_fs::{engine::{Attrs, DirReader, FileHolder}, file::File, mounts::PARTITIONS}; use yazi_shared::url::{UrlBuf, UrlCow, UrlLike, UrlRef}; -use yazi_vfs::{VfsFile, engine}; +use yazi_vfs::engine; use crate::fs::SizeCalculator; @@ -15,6 +15,7 @@ pub fn compose() -> Composer { b"access" => access(lua)?, b"calc_size" => calc_size(lua)?, b"cha" => cha(lua)?, + b"clean_url" => clean_url(lua)?, b"copy" => copy(lua)?, b"create" => create(lua)?, b"cwd" => cwd(lua)?, @@ -25,6 +26,7 @@ pub fn compose() -> Composer { b"read_dir" => read_dir(lua)?, b"remove" => remove(lua)?, b"rename" => rename(lua)?, + b"trash" => return yazi_fs::trash::Trash.into_lua(lua), b"unique" => unique(lua)?, b"write" => write(lua)?, _ => return Ok(Value::Nil), @@ -46,7 +48,7 @@ fn calc_size(lua: &Lua) -> mlua::Result { let it = if let Some(path) = url.as_local() { yazi_fs::engine::local::SizeCalculator::new(path).await.map(SizeCalculator::Local) } else { - yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote) + yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Virtual) }; match it { @@ -57,12 +59,9 @@ fn calc_size(lua: &Lua) -> mlua::Result { } fn cha(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, (url, follow): (UrlRef, Option)| async move { - let cha = if follow.unwrap_or(false) { - engine::metadata(&*url).await - } else { - engine::symlink_metadata(&*url).await - }; + lua.create_async_function(|lua, (url, follow): (UrlRef, bool)| async move { + let cha = + if follow { engine::metadata(&*url).await } else { engine::symlink_metadata(&*url).await }; match cha { Ok(c) => c.into_lua_multi(&lua), @@ -71,6 +70,10 @@ fn cha(lua: &Lua) -> mlua::Result { }) } +fn clean_url(lua: &Lua) -> mlua::Result { + lua.create_function(|_, url: UrlRef| Ok(yazi_fs::path::clean_url(&*url))) +} + fn copy(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move { match engine::copy(&*from, &*to, Attrs::default()).await { @@ -124,7 +127,7 @@ fn expand_url(lua: &Lua) -> mlua::Result { fn file(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - match File::new(&*url).await { + match engine::file(&*url).await { Ok(file) => file.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } @@ -185,8 +188,8 @@ fn read_dir(lua: &Lua) -> mlua::Result { let file = if !resolve { File::from_dummy(url, next.file_type().await.ok()) - } else if let Ok(cha) = next.metadata().await { - File::from_follow(url, cha).await + } else if let Ok(file) = next.file().await { + file } else { File::from_dummy(url, next.file_type().await.ok()) }; diff --git a/yazi-plugin/src/fs/op.rs b/yazi-plugin/src/fs/op.rs index b613eb9e..34260b08 100644 --- a/yazi-plugin/src/fs/op.rs +++ b/yazi-plugin/src/fs/op.rs @@ -31,10 +31,9 @@ impl FilesOp { pub(super) fn done(_: &Lua, t: Table) -> mlua::Result { let id = t.raw_get("id")?; - let cha = t.raw_get("cha")?; - let url = t.raw_get("url")?; + let file = t.raw_get("file")?; - Ok(Self(yazi_fs::FilesOp::Done(url, cha, id))) + Ok(Self(yazi_fs::FilesOp::Done(file, id))) } pub(super) fn size(_: &Lua, t: Table) -> mlua::Result { diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index f8f80ba0..7f843d1d 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -2,29 +2,34 @@ use std::hash::Hash; use mlua::{Function, Lua, Table}; use yazi_config::YAZI; -use yazi_fs::file::FileRef; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_fs::{FsHash128, file::{FileRef, FileSig}}; +use yazi_shared::url::{Url, UrlBuf, UrlLike}; use yazi_shim::Twox128; use super::Utils; impl Utils { pub(super) fn file_cache(lua: &Lua) -> mlua::Result { + struct Sig<'a>(FileSig<'a>, usize); + + impl FsHash128 for Sig<'_> { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.0.hash(&mut h); + self.1.hash(&mut h); + h.finish_128() + } + } + lua.create_function(|_, t: Table| { let file: FileRef = t.raw_get("file")?; file.borrow(|f| { - if f.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { + if f.url.parent() == Some(Url::regular(&YAZI.preview.cache_dir)) { return Ok(None); } - let hex = { - let mut h = Twox128::default(); - f.hash(&mut h); - t.raw_get("skip").unwrap_or(0usize).hash(&mut h); - format!("{:x}", h.finish_128()) - }; - - Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex)))) + let sig = Sig(FileSig(f), t.raw_get("skip").unwrap_or_default()); + Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(sig.hash_u128_str(&mut [0; 26]))))) }) }) } diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index c13d9883..f440f908 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -1,15 +1,18 @@ use mlua::{Function, IntoLuaMulti, Lua, Value}; use yazi_adapter::{ADAPTOR, Image}; use yazi_binding::{Error, ImageInfo, elements::Rect}; -use yazi_fs::FsUrl; -use yazi_shared::url::{AsUrl, UrlLike, UrlRef}; +use yazi_shared::url::{UrlLike, UrlRef}; +use yazi_shim::OptionExt; use super::Utils; impl Utils { pub(super) fn image_info(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - let path = url.as_url().unified_path().into_owned(); + let Some(path) = url.as_local().owned() else { + return (Value::Nil, Error::custom("Source must be a local path")).into_lua_multi(&lua); + }; + match ImageInfo::new(path).await { Ok(info) => info.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), @@ -19,7 +22,10 @@ impl Utils { pub(super) fn image_show(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { - let path = url.as_url().unified_path(); + let Some(path) = url.as_local() else { + return (Value::Nil, Error::custom("Source must be a local path")).into_lua_multi(&lua); + }; + match ADAPTOR.image_show(path, *rect).await { Ok(area) => Rect::from(area).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), @@ -29,11 +35,13 @@ impl Utils { pub(super) fn image_precache(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move { - let Some(dist) = dist.as_local() else { - return (Value::Nil, Error::custom("Destination must be a local path")) - .into_lua_multi(&lua); + let Some(src) = src.as_local().owned() else { + return (false, Error::custom("Source must be a local path")).into_lua_multi(&lua); }; - let src = src.as_url().unified_path().into_owned(); + let Some(dist) = dist.as_local() else { + return (false, Error::custom("Destination must be a local path")).into_lua_multi(&lua); + }; + match Image::precache(src, dist).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua), diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 001bfe04..6fdf4f3d 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -1,9 +1,8 @@ use mlua::{ExternalError, Function, IntoLuaMulti, Lua, Table, Value}; use yazi_binding::{Error, elements::Area}; use yazi_core::{Highlighter, MgrProxy, tab::PreviewLock}; -use yazi_fs::FsUrl; +use yazi_fs::file::FileRef; use yazi_runner::previewer::PeekError; -use yazi_shared::url::AsUrl; use yazi_widgets::Renderable; use super::Utils; @@ -15,9 +14,9 @@ impl Utils { pub(super) fn preview_code(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, t: Table| async move { let area: Area = t.raw_get("area")?; - let mut lock = PreviewLock::try_from(t)?; + let path = t.raw_get::("file")?.borrow(|f| Ok(f.content_path().into_owned()))?; - let path = lock.url.as_url().unified_path(); + let mut lock = PreviewLock::try_from(t)?; let inner = match Highlighter::oneshot(path, lock.skip, area.size()).await { Ok(text) => text, Err(e @ PeekError::Exceeded(max)) => return (e, max).into_lua_multi(&lua), diff --git a/yazi-runner/src/loader/loader.rs b/yazi-runner/src/loader/loader.rs index e4593b46..52b59eab 100644 --- a/yazi-runner/src/loader/loader.rs +++ b/yazi-runner/src/loader/loader.rs @@ -51,6 +51,7 @@ impl Default for Loader { ("pdf".to_owned(), preset!("plugins/pdf").into()), ("session".to_owned(), preset!("plugins/session").into()), ("svg".to_owned(), preset!("plugins/svg").into()), + ("trash".to_owned(), preset!("plugins/trash").into()), ("vfs".to_owned(), preset!("plugins/vfs").into()), ("video".to_owned(), preset!("plugins/video").into()), ("zoxide".to_owned(), preset!("plugins/zoxide").into()), diff --git a/yazi-runner/src/provider/job.rs b/yazi-runner/src/provider/job.rs index 79abdc58..76be4646 100644 --- a/yazi-runner/src/provider/job.rs +++ b/yazi-runner/src/provider/job.rs @@ -3,7 +3,7 @@ use std::io; use mlua::{IntoLua, Lua, Value}; use strum::AsRefStr; use yazi_binding::MpscTx; -use yazi_fs::engine::{Attrs, Demand}; +use yazi_fs::{engine::{Attrs, Demand}, file::File}; use yazi_shared::{path::PathBufDyn, url::UrlBuf}; #[derive(AsRefStr)] @@ -28,6 +28,12 @@ pub enum ProviderJob { ReadDir { url: UrlBuf, }, + Revalidate { + file: File, + }, + File { + url: UrlBuf, + }, Open { url: UrlBuf, attrs: Attrs, @@ -46,6 +52,9 @@ pub enum ProviderJob { RemoveDir { url: UrlBuf, }, + RemoveDirAll { + url: UrlBuf, + }, RemoveFile { url: UrlBuf, }, @@ -99,6 +108,7 @@ impl IntoLua for ProviderJob { match self { Self::Capabilities => {} + Self::Revalidate { file } => t.raw_set("file", file)?, Self::Absolute { url } | Self::Canonicalize { url } @@ -106,9 +116,11 @@ impl IntoLua for ProviderJob { | Self::SymlinkMetadata { url } | Self::Metadata { url } | Self::ReadDir { url } + | Self::File { url } | Self::CreateDir { url } | Self::ReadLink { url } | Self::RemoveDir { url } + | Self::RemoveDirAll { url } | Self::RemoveFile { url } | Self::Trash { url } => t.raw_set("url", url)?, diff --git a/yazi-scheduler/src/fetch/fetch.rs b/yazi-scheduler/src/fetch/fetch.rs index f97ce19c..8dc82089 100644 --- a/yazi-scheduler/src/fetch/fetch.rs +++ b/yazi-scheduler/src/fetch/fetch.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use tokio::sync::mpsc; use tracing::error; use yazi_config::Priority; -use yazi_fs::FsHash64; +use yazi_fs::{FsHash64, file::FileSig}; use yazi_runner::RUNNER; use crate::{HIGH, LOW, TaskOp, TaskOps, fetch::{FetchIn, FetchOutFetch}}; @@ -32,7 +32,7 @@ impl Fetch { pub(crate) async fn fetch(&self, task: FetchIn) -> Result<(), FetchOutFetch> { let (id, fetcher) = (task.id, task.fetcher.clone()); - let hashes: Vec<_> = task.targets.iter().map(|f| f.hash_u64()).collect(); + let hashes: Vec<_> = task.targets.iter().map(|f| FileSig(f).hash_u64()).collect(); let (state, err) = RUNNER.fetch(task.into()).await?; let mut loaded = self.loaded.lock(); diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 5141f54c..5b5a59c9 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -6,7 +6,7 @@ use tracing::warn; use yazi_config::YAZI; use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, engine::{Attrs, Engine, FileHolder, local::Local}, ok_or_not_found, path::path_relative_to}; use yazi_shared::{path::PathCow, url::{AsUrl, UrlCow, UrlLike}}; -use yazi_vfs::{VfsCha, engine::{self, DirEntry}, maybe_exists, unique_file}; +use yazi_vfs::{Stamp, VfsCha, engine::{self, DirEntry}, maybe_exists, unique_file}; use super::{FileInCopy, FileInDelete, FileInHardlink, FileInLink, FileInTrash}; use crate::{LOW, NORMAL, TaskOp, TaskOps, TasksProxy, ctx, file::{FileIn, FileInCut, FileInDownload, FileInUpload, FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo, Transaction, Traverse}, hook::{HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink}, ok_or_not_found}; @@ -342,7 +342,7 @@ impl File { ) -> Result<(), FileOutDownloadDo> { let cha = task.cha.unwrap(); - let cache = ctx!(task, task.target.cache(), "Cannot determine cache path")?; + let cache = ctx!(task, task.target.cache_entry(), "Cannot determine cache path")?; let cache_tmp = ctx!(task, Transaction::tmp(&cache).await, "Cannot determine download cache")?; let mut rx = ctx!(task, engine::copy_progressive(&task.target, &cache_tmp, cha).await)?; @@ -350,12 +350,8 @@ impl File { match rx.recv().await.unwrap_or(Ok(0)) { Ok(0) => { Local::regular(&cache).remove_dir_all().await.ok(); + ctx!(task, Stamp::write(cha, task.target.as_url()).await)?; ctx!(task, engine::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?; - - let lock = ctx!(task, task.target.cache_lock(), "Cannot determine cache lock")?; - let hash = format!("{:x}", cha.hash_u128()); - ctx!(task, Local::regular(&lock).write(hash).await, "Cannot lock cache")?; - break; } Ok(n) => self.ops.out(task.id, FileOutDownloadDo::Adv(n)), @@ -410,13 +406,9 @@ impl File { pub(crate) async fn upload_do(&self, task: FileInUpload) -> Result<(), FileOutUploadDo> { let cha = task.cha.unwrap(); let cache = ctx!(task, task.cache.as_ref(), "Cannot determine cache path")?; - let lock = ctx!(task, task.target.cache_lock(), "Cannot determine cache lock")?; - let hash = ctx!(task, Local::regular(&lock).read_to_string().await, "Cannot read cache lock")?; - let hash = ctx!(task, u128::from_str_radix(&hash, 16), "Cannot parse hash from lock")?; - if hash != cha.hash_u128() { - Err(anyhow!("Failed to work on: {task:?}: remote file has changed since last download"))?; - } + let stamp = ctx!(task, Stamp::read(&task.target).await)?; + ctx!(task, stamp.validate(cha, task.target.as_url()))?; let tmp = ctx!(task, Transaction::tmp(&task.target).await, "Cannot determine temporary upload path")?; @@ -436,7 +428,7 @@ impl File { Ok(0) => { let cha = ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat original file")?; - if hash != cha.hash_u128() { + if stamp.sig() != cha.hash_u128_str(&mut [0; 26]) { Err(anyhow!("Failed to work on: {task:?}: remote file has changed during upload"))?; } @@ -444,8 +436,7 @@ impl File { let cha = ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat uploaded file")?; - let hash = format!("{:x}", cha.hash_u128()); - ctx!(task, Local::regular(&lock).write(hash).await, "Cannot lock cache")?; + ctx!(task, Stamp::write(cha, task.target.as_url()).await)?; break; } diff --git a/yazi-scheduler/src/file/traverse.rs b/yazi-scheduler/src/file/traverse.rs index 866f9571..fab4361c 100644 --- a/yazi-scheduler/src/file/traverse.rs +++ b/yazi-scheduler/src/file/traverse.rs @@ -131,13 +131,13 @@ impl Traverse for FileInUpload { self.cha = Some(super::File::cha(self.from(), self.follow(), None).await?) } if self.cache.is_none() { - self.cache = self.target.cache(); + self.cache = self.target.cache_entry(); } Ok(self.cha.unwrap()) } fn spawn(&self, from: UrlBuf, _to: Option, cha: Cha) -> Self { - Self { id: self.id, cha: Some(cha), cache: from.cache(), target: from } + Self { id: self.id, cha: Some(cha), cache: from.cache_entry(), target: from } } fn to(&self) -> Option> { None } diff --git a/yazi-scheduler/src/preload/preload.rs b/yazi-scheduler/src/preload/preload.rs index 9aad08b2..a740d282 100644 --- a/yazi-scheduler/src/preload/preload.rs +++ b/yazi-scheduler/src/preload/preload.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use tokio::sync::mpsc; use tracing::error; use yazi_config::Priority; -use yazi_fs::FsHash64; +use yazi_fs::{FsHash64, file::FileSig}; use yazi_runner::{RUNNER, preloader::{PreloadError, PreloadJob}}; use yazi_shared::id::Id; @@ -35,7 +35,7 @@ impl Preload { } pub(crate) async fn preload(&self, task: PreloadIn) -> Result<(), PreloadOut> { - let hash = task.target.hash_u64(); + let hash = FileSig(&task.target).hash_u64(); let mut rx = RUNNER.preload(PreloadJob { preloader: task.preloader.clone(), file: task.target }).await; diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 1e2fb594..75beb0aa 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -2,7 +2,7 @@ use std::{ops::Deref, sync::Arc, time::Duration}; use tokio::task::JoinHandle; use yazi_config::{YAZI, plugin::{FetcherArc, PreloaderArc}}; -use yazi_fs::FsHash64; +use yazi_fs::{FsHash64, file::FileSig}; use yazi_shared::{CompletionToken, Throttle, id::Id, url::{UrlBuf, UrlLike}}; use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskIn, TaskProg, Worker, fetch::FetchIn, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::PluginInEntry, preload::PreloadIn, process::{ProcessIn, ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt}, size::SizeIn}; @@ -204,7 +204,7 @@ impl Scheduler { } pub fn preload_paged(&self, preloader: PreloaderArc, target: &yazi_fs::file::File) { - let hook = HookInPreload::new(preloader.idx, target.hash_u64()); + let hook = HookInPreload::new(preloader.idx, FileSig(target).hash_u64()); let mut r#in = PreloadIn { id: Id::ZERO, preloader, target: target.clone() }; self.add_hooked(&mut r#in, hook, |_| ()); diff --git a/yazi-sftp/Cargo.toml b/yazi-sftp/Cargo.toml index aa01cb52..d772f0cd 100644 --- a/yazi-sftp/Cargo.toml +++ b/yazi-sftp/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] bitflags = { workspace = true } +hashbrown = { workspace = true } parking_lot = { workspace = true } russh = { workspace = true } serde = { workspace = true } diff --git a/yazi-sftp/src/fs/attrs.rs b/yazi-sftp/src/fs/attrs.rs index d60acbb2..bfbd057d 100644 --- a/yazi-sftp/src/fs/attrs.rs +++ b/yazi-sftp/src/fs/attrs.rs @@ -1,5 +1,6 @@ -use std::{collections::HashMap, fmt}; +use std::fmt; +use hashbrown::HashMap; use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct}; #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/yazi-sftp/src/requests/init.rs b/yazi-sftp/src/requests/init.rs index 757e2967..1de9000e 100644 --- a/yazi-sftp/src/requests/init.rs +++ b/yazi-sftp/src/requests/init.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; - +use hashbrown::HashMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] diff --git a/yazi-sftp/src/responses/version.rs b/yazi-sftp/src/responses/version.rs index 967ff60a..409f39ca 100644 --- a/yazi-sftp/src/responses/version.rs +++ b/yazi-sftp/src/responses/version.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; - +use hashbrown::HashMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs index 1ad674b5..022783e8 100644 --- a/yazi-sftp/src/session.rs +++ b/yazi-sftp/src/session.rs @@ -1,5 +1,6 @@ -use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc, time::Duration}; +use std::{any::TypeId, io::{self, ErrorKind}, sync::Arc, time::Duration}; +use hashbrown::HashMap; use parking_lot::Mutex; use russh::{ChannelStream, client::Msg}; use serde::Serialize; diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index 16bbebe1..00216569 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -1,7 +1,5 @@ use std::{fmt, hash::{Hash, Hasher}, sync::Arc}; -use anyhow::{Result, anyhow}; -use percent_encoding::percent_decode_str; use serde::Deserialize; use yazi_shim::cell::RoCell; @@ -145,11 +143,4 @@ impl Auth { self.covariant(other) || self.kind == AuthKind::Hub && other.kind == AuthKind::Hub && self.scheme == other.scheme } - - pub fn parse_cache(cache: &str) -> Result> { - let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; - let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; - - Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?)) - } } diff --git a/yazi-shared/src/auth/de.rs b/yazi-shared/src/auth/de.rs new file mode 100644 index 00000000..7d86d948 --- /dev/null +++ b/yazi-shared/src/auth/de.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; + +use super::{Auth, Domain}; +use crate::pool::{InternStr, Symbol}; + +pub(crate) struct AuthDeserializer(pub(crate) Arc); + +impl<'de> Deserializer<'de> for AuthDeserializer { + type Error = de::value::Error; + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes byte_buf + unit unit_struct struct newtype_struct seq tuple tuple_struct enum identifier ignored_any + } + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_map(MapDeserializer::new(self.0)) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_some(self) + } +} + +// --- Map +struct MapDeserializer { + kind: Option<&'static str>, + scheme: Option>, + domain: Option>, + parent: Option>, +} + +impl MapDeserializer { + fn new(auth: Arc) -> Self { + Self { + kind: Some(auth.kind.into()), + scheme: Some(auth.scheme.intern()), + domain: Some(auth.domain.clone()), + parent: auth.parent.clone(), + } + } +} + +impl<'de> MapAccess<'de> for MapDeserializer { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + let key = if self.kind.is_some() { + Some("kind") + } else if self.scheme.is_some() { + Some("scheme") + } else if self.domain.is_some() { + Some("domain") + } else if self.parent.is_some() { + Some("parent") + } else { + None + }; + + key.map(|key| seed.deserialize(key.into_deserializer())).transpose() + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some(kind) = self.kind.take() { + return seed.deserialize(kind.into_deserializer()); + } + if let Some(scheme) = self.scheme.take() { + return seed.deserialize(scheme.into_deserializer()); + } + if let Some(domain) = self.domain.take() { + return seed.deserialize(domain.into_deserializer()); + } + if let Some(parent) = self.parent.take() { + return seed.deserialize(AuthDeserializer(parent)); + } + + Err(de::Error::custom("value missing for key")) + } + + fn size_hint(&self) -> Option { + Some( + self.kind.is_some() as usize + + self.scheme.is_some() as usize + + self.domain.is_some() as usize + + self.parent.is_some() as usize, + ) + } +} diff --git a/yazi-shared/src/auth/domain.rs b/yazi-shared/src/auth/domain.rs index 98297e98..c0bd3a4c 100644 --- a/yazi-shared/src/auth/domain.rs +++ b/yazi-shared/src/auth/domain.rs @@ -1,69 +1,99 @@ -use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref}; +use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref, str}; -use serde::{Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, value::CowStrDeserializer}}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::{self, IntoDeserializer, Visitor}}; -#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(transparent)] -pub struct Domain<'a>(Cow<'a, str>); +use crate::{BytesExt, data::BytesDeserializer}; + +#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Domain<'a>(Cow<'a, [u8]>); impl Domain<'static> { - pub const CATCHALL: Self = Self(Cow::Borrowed("*")); - pub const EMPTY: Self = Self(Cow::Borrowed("")); -} - -impl Domain<'_> { - #[inline] - pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) } - - #[inline] - pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL } + pub const CATCHALL: Self = Self(Cow::Borrowed(b"*")); + pub const EMPTY: Self = Self(Cow::Borrowed(b"")); } impl Deref for Domain<'_> { - type Target = str; + type Target = [u8]; fn deref(&self) -> &Self::Target { &self.0 } } -impl Borrow for Domain<'_> { - fn borrow(&self) -> &str { &self.0 } +impl Borrow<[u8]> for Domain<'_> { + fn borrow(&self) -> &[u8] { &self.0 } } -impl AsRef for Domain<'_> { - fn as_ref(&self) -> &str { &self.0 } +impl AsRef<[u8]> for Domain<'_> { + fn as_ref(&self) -> &[u8] { &self.0 } +} + +impl<'a> From> for Domain<'a> { + fn from(value: Vec) -> Self { Self(Cow::Owned(value)) } +} + +impl<'a> From> for Domain<'a> { + fn from(value: Cow<'a, [u8]>) -> Self { Self(value) } } impl<'a> From for Domain<'a> { - fn from(value: String) -> Self { Self(Cow::Owned(value)) } + fn from(value: String) -> Self { Self(Cow::Owned(value.into_bytes())) } } -impl<'a> From<&'a str> for Domain<'a> { - fn from(value: &'a str) -> Self { Self(Cow::Borrowed(value)) } -} - -impl<'a> From> for Domain<'a> { - fn from(value: Cow<'a, str>) -> Self { Self(value) } -} - -impl<'a> From<&'a Domain<'_>> for Domain<'a> { - fn from(value: &'a Domain<'_>) -> Self { Self(Cow::Borrowed(&value.0)) } +impl<'a, T> From<&'a T> for Domain<'a> +where + T: AsRef<[u8]> + ?Sized, +{ + fn from(value: &'a T) -> Self { Self(Cow::Borrowed(value.as_ref())) } } impl Display for Domain<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) } + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.display().fmt(f) } +} + +impl<'a> Domain<'a> { + pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) } + + pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL } + + pub fn to_str(&self) -> Result<&str, str::Utf8Error> { str::from_utf8(&self.0) } +} + +impl<'de, 'a: 'de> IntoDeserializer<'de, de::value::Error> for Domain<'a> { + type Deserializer = BytesDeserializer<'a>; + + fn into_deserializer(self) -> Self::Deserializer { BytesDeserializer(self.0) } +} + +impl Serialize for Domain<'_> { + fn serialize(&self, serializer: S) -> Result { + match self.to_str() { + Ok(s) => serializer.serialize_str(s), + Err(_) => serializer.serialize_bytes(&self.0), + } + } } impl<'de> Deserialize<'de> for Domain<'_> { fn deserialize>(deserializer: D) -> Result { - Ok(Self(Cow::Owned(String::deserialize(deserializer)?))) + struct V; + + impl Visitor<'_> for V { + type Value = Domain<'static>; + + fn expecting(&self, f: &mut Formatter) -> fmt::Result { f.write_str("a string or bytes") } + + fn visit_str(self, v: &str) -> Result { + Ok(v.to_owned().into()) + } + + fn visit_string(self, v: String) -> Result { Ok(v.into()) } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_owned().into()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { Ok(v.into()) } + } + + deserializer.deserialize_any(V) } } - -impl<'de, 'a, E> IntoDeserializer<'de, E> for Domain<'a> -where - E: de::Error, -{ - type Deserializer = CowStrDeserializer<'a, E>; - - fn into_deserializer(self) -> Self::Deserializer { self.0.into_deserializer() } -} diff --git a/yazi-shared/src/auth/encode.rs b/yazi-shared/src/auth/encode.rs index 662a8b0e..53ac74f7 100644 --- a/yazi-shared/src/auth/encode.rs +++ b/yazi-shared/src/auth/encode.rs @@ -1,15 +1,16 @@ -use std::fmt; +use std::fmt::{self, Display}; -use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; +use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; use super::{Auth, AuthKind, Domain}; +// --- EncodeAuth pub struct EncodeAuth<'a>(pub &'a Auth, pub bool); impl EncodeAuth<'_> { - pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + pub fn domain<'a>(s: &'a Domain<'_>) -> EncodeDomain<'a> { const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%'); - percent_encode(s.as_bytes(), SET) + EncodeDomain(s, SET) } } @@ -29,9 +30,9 @@ impl fmt::Display for EncodeAuth<'_> { pub struct EncodePrefix<'a>(pub &'a Auth); impl EncodePrefix<'_> { - pub fn parent<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + pub fn parent<'a>(s: &'a Domain<'_>) -> EncodeDomain<'a> { const SET: &AsciiSet = &CONTROLS.add(b'/').add(b',').add(b'@').add(b'%'); - percent_encode(s.as_bytes(), SET) + EncodeDomain(s, SET) } } @@ -53,3 +54,22 @@ impl fmt::Display for EncodePrefix<'_> { f.write_str("/") } } + +// --- EncodeDomain +pub struct EncodeDomain<'a>(&'a [u8], &'static AsciiSet); + +impl Display for EncodeDomain<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for chunk in self.0.utf8_chunks() { + for c in chunk.valid().chars() { + if c.is_ascii() { + percent_encode(&[c as u8], self.1).fmt(f)?; + } else { + c.fmt(f)?; + } + } + percent_encode(chunk.invalid(), self.1).fmt(f)?; + } + Ok(()) + } +} diff --git a/yazi-shared/src/auth/mod.rs b/yazi-shared/src/auth/mod.rs index 8ce33bb5..0feefdb2 100644 --- a/yazi-shared/src/auth/mod.rs +++ b/yazi-shared/src/auth/mod.rs @@ -1,3 +1,3 @@ -yazi_macro::mod_flat!(auth domain encode inventory kind scheme); +yazi_macro::mod_flat!(auth de domain encode inventory kind scheme); pub(super) fn init() { DEFAULT_ARC.with(Default::default); } diff --git a/yazi-shared/src/data/de_bytes.rs b/yazi-shared/src/data/de_bytes.rs index 3dcee3c4..5ea9fd63 100644 --- a/yazi-shared/src/data/de_bytes.rs +++ b/yazi-shared/src/data/de_bytes.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, value::SeqDeserializer}}; -pub(crate) struct BytesDeserializer<'a>(pub(crate) Cow<'a, [u8]>); +pub struct BytesDeserializer<'a>(pub(crate) Cow<'a, [u8]>); impl<'de, 'a: 'de> Deserializer<'de> for BytesDeserializer<'a> { type Error = de::value::Error; diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs index 007885eb..e674c1f5 100644 --- a/yazi-shared/src/path/error.rs +++ b/yazi-shared/src/path/error.rs @@ -53,6 +53,10 @@ impl From for std::io::Error { fn from(err: PathDynError) -> Self { Self::other(err) } } +impl From for mlua::Error { + fn from(err: PathDynError) -> Self { Self::external(err) } +} + // --- SetNameError #[derive(Debug, Error)] #[error("calling set_name on paths with different encodings")] diff --git a/yazi-shared/src/path/lua.rs b/yazi-shared/src/path/lua.rs index c53d9ffe..aedfd9eb 100644 --- a/yazi-shared/src/path/lua.rs +++ b/yazi-shared/src/path/lua.rs @@ -1,5 +1,5 @@ use mlua::{AnyUserData, ExternalError, ExternalResult, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; -use yazi_shim::mlua::UserDataFieldsExt; +use yazi_shim::{OptionExt, mlua::UserDataFieldsExt}; use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, strand::{AsStrand, StrandCow}}; @@ -74,7 +74,7 @@ impl UserData for PathBufDyn { fields.add_cached_field("name", |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); - fields.add_cached_field("parent", |_, me| Ok(me.parent().map(|p| p.to_owned()))); + fields.add_cached_field("parent", |_, me| Ok(me.parent().owned())); fields.add_cached_field("stem", |lua, me| { me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); @@ -92,6 +92,7 @@ impl UserData for PathBufDyn { methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: LuaString| { lua.create_external_string([lhs.encoded_bytes(), &rhs.as_bytes()].concat()) }); + methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); methods.add_meta_method(MetaMethod::Eq, |_, me, other: PathRef| Ok(*me == *other)); methods .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index f8cb1b83..0311fafe 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -2,6 +2,7 @@ use std::{borrow::Cow, ffi::OsStr}; use anyhow::Result; use hashbrown::Equivalent; +use yazi_shim::OptionExt; use super::{RsplitOnceError, StartsWithError}; use crate::{BytesExt, Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; @@ -55,6 +56,10 @@ impl Equivalent for PathDyn<'_> { fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.dyn_path() } } +impl<'p> OptionExt for Option> { + fn owned(self) -> Option { self.map(PathDyn::to_owned) } +} + impl<'p> PathDyn<'p> { #[inline] pub fn as_os(self) -> Result<&'p std::path::Path, PathDynError> { diff --git a/yazi-shared/src/spec/spec.rs b/yazi-shared/src/spec/spec.rs index 24b0b5f2..a20aca99 100644 --- a/yazi-shared/src/spec/spec.rs +++ b/yazi-shared/src/spec/spec.rs @@ -65,9 +65,9 @@ impl Spec { *skip += len + slash as usize; let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; - let domain = percent_decode(&bytes[..len]).decode_utf8()?.into(); + let domain: Cow<[u8]> = percent_decode(&bytes[..len]).into(); - Ok((domain, uri, urn)) + Ok((domain.into(), uri, urn)) } fn decode_ports(bytes: &[u8], skip: &mut usize) -> Result<(Option, Option)> { @@ -117,7 +117,7 @@ impl Spec { parent = Some(Arc::new(Auth { kind: auth.kind, scheme: auth.scheme.clone(), - domain: Domain::from(percent_decode(domain).decode_utf8()?).into_owned(), + domain: Domain::from(Cow::from(percent_decode(domain))).into_owned(), parent, })); } diff --git a/yazi-shared/src/strand/error.rs b/yazi-shared/src/strand/error.rs index d66e86d1..5001275d 100644 --- a/yazi-shared/src/strand/error.rs +++ b/yazi-shared/src/strand/error.rs @@ -12,3 +12,7 @@ pub enum StrandError { impl From for std::io::Error { fn from(err: StrandError) -> Self { Self::other(err) } } + +impl From for mlua::Error { + fn from(err: StrandError) -> Self { Self::external(err) } +} diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index b66d341f..16fdf6f1 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -1,7 +1,7 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::Display}; +use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::Display}; use anyhow::Result; -use yazi_shim::wtf8::FromWtf8; +use yazi_shim::{OptionExt, wtf8::FromWtf8}; use crate::{BytesExt, strand::{AsStrand, StrandBuf, StrandError, StrandKind}}; @@ -79,6 +79,10 @@ impl PartialEq<&[u8]> for Strand<'_> { } } +impl<'p> OptionExt for Option> { + fn owned(self) -> Option { self.map(Strand::to_owned) } +} + impl<'a> Strand<'a> { pub fn as_os(self) -> Result<&'a OsStr, StrandError> { match self { @@ -88,6 +92,9 @@ impl<'a> Strand<'a> { } } + #[inline] + pub fn to_os_string(self) -> Result { self.as_os().map(|s| s.to_owned()) } + #[inline] pub fn as_os_path(self) -> Result<&'a std::path::Path, StrandError> { self.as_os().map(std::path::Path::new) diff --git a/yazi-shared/src/tests.rs b/yazi-shared/src/tests.rs index e3360402..fc0c0f49 100644 --- a/yazi-shared/src/tests.rs +++ b/yazi-shared/src/tests.rs @@ -11,10 +11,10 @@ pub fn init_tests() { inventory::submit! { AuthInventory { get: |scheme, domain| match (scheme.as_str(), domain.as_ref()) { - ("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), + ("test-mount", b"7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), ("test-hub", _) => Some(Auth::new(AuthKind::Hub, scheme.clone(), domain.clone())), - ("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), - ("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), + ("test-scope", b"aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), + ("sftp", b"vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), _ => None, }, } diff --git a/yazi-shared/src/url/de.rs b/yazi-shared/src/url/de.rs index db494024..8a4147da 100644 --- a/yazi-shared/src/url/de.rs +++ b/yazi-shared/src/url/de.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; -use crate::{auth::Domain, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; +use crate::{auth::{AuthDeserializer, Domain}, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>); @@ -36,6 +36,7 @@ struct MapDeserializer<'a> { domain: Option>, uri: Option, urn: Option, + parent: Option, path: Option>, } @@ -50,6 +51,7 @@ impl<'a> MapDeserializer<'a> { domain: Some(spec.domain.clone()), uri: Some(uri), urn: Some(urn), + parent: spec.auth.parent.clone().map(AuthDeserializer), path: Some(path.into_encoded_bytes()), } } @@ -72,6 +74,8 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { Some("uri") } else if self.urn.is_some() { Some("urn") + } else if self.parent.is_some() { + Some("parent") } else if self.path.is_some() { Some("path") } else { @@ -100,6 +104,9 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { if let Some(urn) = self.urn.take() { return seed.deserialize(urn.into_deserializer()); } + if let Some(parent) = self.parent.take() { + return seed.deserialize(parent); + } if let Some(path) = self.path.take() { return seed.deserialize(BytesDeserializer(path)); } @@ -114,6 +121,7 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { + self.domain.is_some() as usize + self.uri.is_some() as usize + self.urn.is_some() as usize + + self.parent.is_some() as usize + self.path.is_some() as usize, ) } diff --git a/yazi-shared/src/url/lua.rs b/yazi-shared/src/url/lua.rs index 2ad5777c..0e623f4d 100644 --- a/yazi-shared/src/url/lua.rs +++ b/yazi-shared/src/url/lua.rs @@ -1,4 +1,4 @@ -use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; +use mlua::{AnyUserData, BorrowedBytes, ExternalError, ExternalResult, IntoLua, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; use yazi_shim::mlua::UserDataFieldsExt; use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, spec::Spec, strand::{StrandLike, ToStrand}, url::{UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; @@ -18,7 +18,7 @@ impl UrlBuf { if let Ok(url) = ud.borrow::() { url.clone() } else if let Ok(path) = ud.borrow::() { - path.as_os().into_lua_err()?.into() + path.as_os()?.into() } else { Err(EXPECTED.into_lua_err())? } @@ -121,8 +121,8 @@ impl UserData for UrlBuf { methods.add_method_once("into_search", |_, me, domain: LuaString| { me.into_search(domain.to_str()?).into_lua_err() }); - methods.add_method_once("into_domain", |_, me, domain: LuaString| { - Ok(me.into_domain(domain.to_str()?.to_owned())) + methods.add_method_once("into_domain", |_, me, domain: BorrowedBytes| { + Ok(me.into_domain(domain.to_vec())) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other)); diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index c5a30b76..63a3e568 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -104,7 +104,7 @@ pub trait UrlMapExt { fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V where U: AsUrl, - F: FnOnce(Url<'_>) -> V; + F: FnOnce(U) -> V; } impl UrlMapExt for HashMap @@ -122,12 +122,12 @@ where fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V where U: AsUrl, - F: FnOnce(Url<'_>) -> V, + F: FnOnce(U) -> V, { - let url = url.as_url(); - match self.entry_ref(&url) { - EntryRef::Occupied(oe) => oe.into_mut(), - EntryRef::Vacant(ve) => ve.insert_with_key(url.into(), default(url)), + let key = url.as_url(); + match self.raw_entry_mut().from_key(&key) { + hashbrown::hash_map::RawEntryMut::Occupied(oe) => oe.into_mut(), + hashbrown::hash_map::RawEntryMut::Vacant(ve) => ve.insert(key.into(), default(url)).1, } } } @@ -147,10 +147,13 @@ where fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V where U: AsUrl, - F: FnOnce(Url<'_>) -> V, + F: FnOnce(U) -> V, { - let url = url.as_url(); - self.raw_entry_mut_v1().from_key(&url).or_insert_with(|| (url.into(), default(url))).1 + let key = url.as_url(); + match self.raw_entry_mut_v1().from_key(&key) { + indexmap::map::raw_entry_v1::RawEntryMut::Occupied(oe) => oe.into_mut(), + indexmap::map::raw_entry_v1::RawEntryMut::Vacant(ve) => ve.insert(key.into(), default(url)).1, + } } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index db3d1dbe..44fcc8ba 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -194,7 +194,7 @@ impl<'a> Url<'a> { #[inline] pub fn entry_key(self) -> PathDyn<'a> { match self { - Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(auth.domain.as_bytes())), + Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(&auth.domain)), _ => self.urn(), } } diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index 6c07000c..e36bd485 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -36,3 +36,6 @@ unicode-width = { workspace = true } [dependencies.unicode-segmentation] version = "1.13.3" + +[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))'.dependencies] +trash = "5.2.6" diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index 6a1c09c3..c5dabb9b 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(arc_swap cell fs mlua path ratatui serde strum toml vec wtf8); -yazi_macro::mod_flat!(base64 percent_encoding sstr twox utf8); +yazi_macro::mod_flat!(base64 option percent_encoding sstr twox utf8); #[cfg(windows)] yazi_macro::mod_flat!(win32); diff --git a/yazi-shim/src/option.rs b/yazi-shim/src/option.rs new file mode 100644 index 00000000..ed2f2251 --- /dev/null +++ b/yazi-shim/src/option.rs @@ -0,0 +1,7 @@ +pub trait OptionExt { + fn owned(self) -> Option; +} + +impl OptionExt for Option<&T> { + fn owned(self) -> Option { self.map(ToOwned::to_owned) } +} diff --git a/yazi-vfs/src/cha.rs b/yazi-vfs/src/cha.rs index 639b430d..fb8ae768 100644 --- a/yazi-vfs/src/cha.rs +++ b/yazi-vfs/src/cha.rs @@ -1,6 +1,6 @@ use std::io; -use yazi_fs::cha::{Cha, ChaKind}; +use yazi_fs::cha::Cha; use yazi_shared::url::AsUrl; use crate::engine; @@ -20,18 +20,12 @@ impl VfsCha for Cha { Ok(Self::from_follow(url, engine::symlink_metadata(url).await?).await) } - async fn from_follow(url: U, mut cha: Self) -> Self + async fn from_follow(url: U, cha: Self) -> Self where U: AsUrl, { let url = url.as_url(); - let mut retain = cha.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM); - - if cha.is_link() { - retain |= ChaKind::FOLLOW; - cha = engine::metadata(url).await.unwrap_or(cha); - } - - cha.attach(retain) + let followed = if cha.is_link() { engine::metadata(url).await.ok() } else { None }; + cha.follow(followed) } } diff --git a/yazi-vfs/src/engine/demand.rs b/yazi-vfs/src/engine/demand.rs index 02120a53..372e86dc 100644 --- a/yazi-vfs/src/engine/demand.rs +++ b/yazi-vfs/src/engine/demand.rs @@ -38,12 +38,14 @@ impl FileBuilder for Demand { let url = url.as_url(); Ok(match url.kind() { AuthKind::Regular | AuthKind::Search => { - self.0.build::().open(url).await?.into() + (self.0.build::().open(url).await?, url.to_owned()).into() } AuthKind::Mount | AuthKind::Hub | AuthKind::Scope => { self.0.build::().open(url).await?.into() } - AuthKind::Sftp => self.0.build::().open(url).await?.into(), + AuthKind::Sftp => { + (self.0.build::().open(url).await?, url.to_owned()).into() + } }) } diff --git a/yazi-vfs/src/engine/dir_entry.rs b/yazi-vfs/src/engine/dir_entry.rs index 2a8b8a86..a8e79bda 100644 --- a/yazi-vfs/src/engine/dir_entry.rs +++ b/yazi-vfs/src/engine/dir_entry.rs @@ -1,6 +1,6 @@ use std::io; -use yazi_fs::{cha::{Cha, ChaType}, engine::FileHolder}; +use yazi_fs::{cha::{Cha, ChaType}, engine::FileHolder, file::File}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::UrlBuf}; pub enum DirEntry { @@ -10,6 +10,14 @@ pub enum DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { + match self { + Self::Local(entry) => entry.file().await, + Self::Lua(entry) => entry.file().await, + Self::Sftp(entry) => entry.file().await, + } + } + async fn file_type(&self) -> io::Result { match self { Self::Local(entry) => entry.file_type().await, diff --git a/yazi-vfs/src/engine/engine.rs b/yazi-vfs/src/engine/engine.rs index b727b682..e4c36dba 100644 --- a/yazi-vfs/src/engine/engine.rs +++ b/yazi-vfs/src/engine/engine.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine, local::Local}}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine, local::Local}, file::File}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; use super::{Engines, ReadDir, RwFile}; @@ -113,6 +113,13 @@ where Engines::new(url.as_url()).await?.create_new().await } +pub async fn file(url: U) -> io::Result +where + U: AsUrl, +{ + Engines::new(url.as_url()).await?.file().await +} + pub async fn hard_link(original: U, link: V) -> io::Result<()> where U: AsUrl, @@ -174,6 +181,10 @@ where Engines::new(url.as_url()).await?.read_link().await } +pub async fn revalidate(file: &File) -> io::Result> { + Engines::new(file.as_url()).await?.revalidate(file.clone()).await +} + pub async fn remove_dir(url: U) -> io::Result<()> where U: AsUrl, diff --git a/yazi-vfs/src/engine/engines.rs b/yazi-vfs/src/engine/engines.rs index 36a8d722..e948ebcf 100644 --- a/yazi-vfs/src/engine/engines.rs +++ b/yazi-vfs/src/engine/engines.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::File}; use yazi_shared::{path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] @@ -74,10 +74,11 @@ impl<'a> Engine for Engines<'a> { } async fn create(&self) -> io::Result { + let url = self.url(); Ok(match self { - Self::Local(p) => p.create().await?.into(), + Self::Local(p) => (p.create().await?, url.to_owned()).into(), Self::Lua(p) => p.create().await?.into(), - Self::Sftp(p) => p.create().await?.into(), + Self::Sftp(p) => (p.create().await?, url.to_owned()).into(), }) } @@ -98,13 +99,22 @@ impl<'a> Engine for Engines<'a> { } async fn create_new(&self) -> io::Result { + let url = self.url(); Ok(match self { - Self::Local(p) => p.create_new().await?.into(), + Self::Local(p) => (p.create_new().await?, url.to_owned()).into(), Self::Lua(p) => p.create_new().await?.into(), - Self::Sftp(p) => p.create_new().await?.into(), + Self::Sftp(p) => (p.create_new().await?, url.to_owned()).into(), }) } + async fn file(&self) -> io::Result { + match self { + Self::Local(p) => p.file().await, + Self::Lua(p) => p.file().await, + Self::Sftp(p) => p.file().await, + } + } + async fn hard_link

(&self, to: P) -> io::Result<()> where P: DynPath, @@ -135,10 +145,11 @@ impl<'a> Engine for Engines<'a> { } async fn open(&self) -> io::Result { + let url = self.url(); Ok(match self { - Self::Local(p) => p.open().await?.into(), + Self::Local(p) => (p.open().await?, url.to_owned()).into(), Self::Lua(p) => p.open().await?.into(), - Self::Sftp(p) => p.open().await?.into(), + Self::Sftp(p) => (p.open().await?, url.to_owned()).into(), }) } @@ -158,6 +169,14 @@ impl<'a> Engine for Engines<'a> { } } + async fn revalidate(&self, file: File) -> io::Result> { + match self { + Self::Local(p) => p.revalidate(file).await, + Self::Lua(p) => p.revalidate(file).await, + Self::Sftp(p) => p.revalidate(file).await, + } + } + async fn remove_dir(&self) -> io::Result<()> { match self { Self::Local(p) => p.remove_dir().await, diff --git a/yazi-vfs/src/engine/lua/file.rs b/yazi-vfs/src/engine/lua/file.rs index b503ddc7..d2d3b35b 100644 --- a/yazi-vfs/src/engine/lua/file.rs +++ b/yazi-vfs/src/engine/lua/file.rs @@ -51,6 +51,16 @@ impl File { Ok(RUNNER.provide(self.run, ProviderJob::Metadata { url }).await.0?) } + pub(crate) async fn file(&self) -> io::Result { + let url = self.url.clone(); + Ok(RUNNER.provide(self.run, ProviderJob::File { url }).await.0?) + } + + pub(crate) async fn into_file(self) -> io::Result { + let url = self.url; + Ok(RUNNER.provide(self.run, ProviderJob::File { url }).await.0?) + } + fn write_impl( self: Pin<&mut Self>, cx: &mut Context<'_>, diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs index 81373ec9..4fd15dc3 100644 --- a/yazi-vfs/src/engine/lua/lua.rs +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -4,7 +4,7 @@ use mlua::FromLua; use tokio::sync::mpsc; use yazi_binding::MpscTx; use yazi_config::vfs::{ServiceLua, Vfs}; -use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::{File, Files}}; use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}}; use yazi_shared::{event::Cmd, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; @@ -86,6 +86,12 @@ impl<'a> Engine for Lua<'a> { Ok(self.call(ProviderJob::CreateDir { url }).await?.ok()?) } + async fn file(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::File { url }).await?.0?) + } + async fn hard_link

(&self, to: P) -> io::Result<()> where P: DynPath, @@ -127,6 +133,10 @@ impl<'a> Engine for Lua<'a> { Ok(self.call(ProviderJob::ReadLink { url }).await?.0?) } + async fn revalidate(&self, file: File) -> io::Result> { + Ok(self.call(ProviderJob::Revalidate { file }).await?.0?) + } + async fn remove_dir(&self) -> io::Result<()> { let url = self.url.to_owned(); diff --git a/yazi-vfs/src/engine/lua/read_dir.rs b/yazi-vfs/src/engine/lua/read_dir.rs index 86a99dea..a25a28fe 100644 --- a/yazi-vfs/src/engine/lua/read_dir.rs +++ b/yazi-vfs/src/engine/lua/read_dir.rs @@ -21,6 +21,8 @@ pub struct DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { Ok(self.file.clone()) } + async fn file_type(&self) -> io::Result { Ok(**self.file.cha) } async fn metadata(&self) -> io::Result { Ok(self.file.cha) } diff --git a/yazi-vfs/src/engine/rw_file.rs b/yazi-vfs/src/engine/rw_file.rs index 20809cf5..45b4923d 100644 --- a/yazi-vfs/src/engine/rw_file.rs +++ b/yazi-vfs/src/engine/rw_file.rs @@ -3,20 +3,23 @@ use std::{io, pin::Pin}; use mlua::{IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt}; use yazi_binding::Error; -use yazi_fs::engine::Attrs; +use yazi_fs::{engine::Attrs, file::File}; +use yazi_shared::url::{UrlBuf, UrlLike}; + +use crate::VfsFile; pub enum RwFile { - Tokio(tokio::fs::File), - Sftp(Box), + Tokio(tokio::fs::File, UrlBuf), + Sftp(Box, UrlBuf), Lua(super::lua::File), } -impl From for RwFile { - fn from(f: tokio::fs::File) -> Self { Self::Tokio(f) } +impl From<(tokio::fs::File, UrlBuf)> for RwFile { + fn from((f, url): (tokio::fs::File, UrlBuf)) -> Self { Self::Tokio(f, url) } } -impl From for RwFile { - fn from(f: yazi_sftp::fs::File) -> Self { Self::Sftp(Box::new(f)) } +impl From<(yazi_sftp::fs::File, UrlBuf)> for RwFile { + fn from((f, url): (yazi_sftp::fs::File, UrlBuf)) -> Self { Self::Sftp(Box::new(f), url) } } impl From for RwFile { @@ -24,18 +27,44 @@ impl From for RwFile { } impl RwFile { - // FIXME: path pub async fn metadata(&self) -> io::Result { Ok(match self { - Self::Tokio(f) => yazi_fs::cha::Cha::new("// FIXME", f.metadata().await?), - Self::Sftp(f) => super::sftp::Cha::try_from(("// FIXME".as_bytes(), &f.fstat().await?))?.0, + Self::Tokio(f, url) => { + yazi_fs::cha::Cha::new(url.name().unwrap_or_default(), f.metadata().await?) + } + Self::Sftp(f, url) => { + let name = url.name().unwrap_or_default().encoded_bytes(); + super::sftp::Cha::try_from((name, &f.fstat().await?))?.0 + } Self::Lua(f) => f.metadata().await?, }) } + pub async fn file(&self) -> io::Result { + Ok(match self { + Self::Tokio(_, url) | Self::Sftp(_, url) => { + let cha = self.metadata().await?; + File::from_follow(url.clone(), cha).await + } + Self::Lua(f) => f.file().await?, + }) + } + + pub async fn into_file(self) -> io::Result { + if let Self::Lua(f) = self { + return f.into_file().await; + } + + let cha = self.metadata().await?; + Ok(match self { + Self::Tokio(_, url) | Self::Sftp(_, url) => File { url, cha, extra: Default::default() }, + Self::Lua(_) => unreachable!(), + }) + } + pub async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { match self { - Self::Tokio(f) => { + Self::Tokio(f, _) => { let (perm, times) = (attrs.try_into(), attrs.try_into()); if perm.is_err() && times.is_err() { return Ok(()); @@ -48,7 +77,7 @@ impl RwFile { }) .await?; } - Self::Sftp(f) => { + Self::Sftp(f, _) => { if let Ok(attrs) = super::sftp::Attrs(attrs).try_into() { f.fsetstat(&attrs).await?; } @@ -61,8 +90,8 @@ impl RwFile { pub async fn set_len(&self, size: u64) -> io::Result<()> { Ok(match self { - Self::Tokio(f) => f.set_len(size).await?, - Self::Sftp(f) => { + Self::Tokio(f, _) => f.set_len(size).await?, + Self::Sftp(f, _) => { f.fsetstat(&yazi_sftp::fs::Attrs { size: Some(size), ..Default::default() }).await? } Self::Lua(f) => f.set_len(size).await?, @@ -78,9 +107,9 @@ impl AsyncRead for RwFile { buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_read(cx, buf), - Self::Sftp(f) => Pin::new(f).poll_read(cx, buf), - Self::Lua(f) => Pin::new(f).poll_read(cx, buf), + RwFile::Tokio(f, _) => Pin::new(f).poll_read(cx, buf), + RwFile::Sftp(f, _) => Pin::new(f).poll_read(cx, buf), + RwFile::Lua(f) => Pin::new(f).poll_read(cx, buf), } } } @@ -89,9 +118,9 @@ impl AsyncSeek for RwFile { #[inline] fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { match &mut *self { - Self::Tokio(f) => Pin::new(f).start_seek(position), - Self::Sftp(f) => Pin::new(f).start_seek(position), - Self::Lua(f) => Pin::new(f).start_seek(position), + RwFile::Tokio(f, _) => Pin::new(f).start_seek(position), + RwFile::Sftp(f, _) => Pin::new(f).start_seek(position), + RwFile::Lua(f) => Pin::new(f).start_seek(position), } } @@ -101,9 +130,9 @@ impl AsyncSeek for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_complete(cx), - Self::Sftp(f) => Pin::new(f).poll_complete(cx), - Self::Lua(f) => Pin::new(f).poll_complete(cx), + RwFile::Tokio(f, _) => Pin::new(f).poll_complete(cx), + RwFile::Sftp(f, _) => Pin::new(f).poll_complete(cx), + RwFile::Lua(f) => Pin::new(f).poll_complete(cx), } } } @@ -116,9 +145,9 @@ impl AsyncWrite for RwFile { buf: &[u8], ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_write(cx, buf), - Self::Sftp(f) => Pin::new(f).poll_write(cx, buf), - Self::Lua(f) => Pin::new(f).poll_write(cx, buf), + RwFile::Tokio(f, _) => Pin::new(f).poll_write(cx, buf), + RwFile::Sftp(f, _) => Pin::new(f).poll_write(cx, buf), + RwFile::Lua(f) => Pin::new(f).poll_write(cx, buf), } } @@ -128,9 +157,9 @@ impl AsyncWrite for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_flush(cx), - Self::Sftp(f) => Pin::new(f).poll_flush(cx), - Self::Lua(f) => Pin::new(f).poll_flush(cx), + RwFile::Tokio(f, _) => Pin::new(f).poll_flush(cx), + RwFile::Sftp(f, _) => Pin::new(f).poll_flush(cx), + RwFile::Lua(f) => Pin::new(f).poll_flush(cx), } } @@ -140,9 +169,9 @@ impl AsyncWrite for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_shutdown(cx), - Self::Sftp(f) => Pin::new(f).poll_shutdown(cx), - Self::Lua(f) => Pin::new(f).poll_shutdown(cx), + RwFile::Tokio(f, _) => Pin::new(f).poll_shutdown(cx), + RwFile::Sftp(f, _) => Pin::new(f).poll_shutdown(cx), + RwFile::Lua(f) => Pin::new(f).poll_shutdown(cx), } } @@ -153,18 +182,18 @@ impl AsyncWrite for RwFile { bufs: &[io::IoSlice<'_>], ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs), - Self::Sftp(f) => Pin::new(f).poll_write_vectored(cx, bufs), - Self::Lua(f) => Pin::new(f).poll_write_vectored(cx, bufs), + RwFile::Tokio(f, _) => Pin::new(f).poll_write_vectored(cx, bufs), + RwFile::Sftp(f, _) => Pin::new(f).poll_write_vectored(cx, bufs), + RwFile::Lua(f) => Pin::new(f).poll_write_vectored(cx, bufs), } } #[inline] fn is_write_vectored(&self) -> bool { match self { - Self::Tokio(f) => f.is_write_vectored(), - Self::Sftp(f) => f.is_write_vectored(), - Self::Lua(f) => f.is_write_vectored(), + RwFile::Tokio(f, _) => f.is_write_vectored(), + RwFile::Sftp(f, _) => f.is_write_vectored(), + RwFile::Lua(f) => f.is_write_vectored(), } } } diff --git a/yazi-vfs/src/engine/sftp/read_dir.rs b/yazi-vfs/src/engine/sftp/read_dir.rs index 0d5b0100..e1530e1a 100644 --- a/yazi-vfs/src/engine/sftp/read_dir.rs +++ b/yazi-vfs/src/engine/sftp/read_dir.rs @@ -1,9 +1,10 @@ use std::{io, sync::Arc}; -use yazi_fs::engine::{DirReader, FileHolder}; +use yazi_fs::{engine::{DirReader, FileHolder}, file::File}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use super::{Cha, ChaMode}; +use crate::VfsFile; pub struct ReadDir { pub(super) dir: Arc, @@ -25,6 +26,11 @@ pub struct DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { + let cha = self.metadata().await?; + Ok(File::from_follow(self.url(), cha).await) + } + async fn file_type(&self) -> io::Result { Ok(ChaMode::try_from(self.entry.attrs())?.0.into()) } diff --git a/yazi-vfs/src/entries.rs b/yazi-vfs/src/entries.rs index 290841a6..cd2247ec 100644 --- a/yazi-vfs/src/entries.rs +++ b/yazi-vfs/src/entries.rs @@ -1,17 +1,17 @@ use std::io; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::{Entries, FilesOp, cha::Cha, engine::{DirReader, FileHolder}, file::File, mounts::PARTITIONS}; +use yazi_fs::{Entries, engine::{DirReader, FileHolder}, file::File, mounts::PARTITIONS}; use yazi_shared::url::UrlBuf; -use crate::{VfsCha, VfsFile, VfsFilesOp, engine::{self, DirEntry}}; +use crate::engine::{self, DirEntry}; pub trait VfsEntries { fn from_dir(dir: &UrlBuf) -> impl Future>>; fn from_dir_bulk(dir: &UrlBuf) -> impl Future>>; - fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future>; + fn revalidate(file: &File) -> impl Future>>; } impl VfsEntries for Entries { @@ -23,11 +23,10 @@ impl VfsEntries for Entries { while let Ok(Some(ent)) = it.next().await { select! { _ = tx.closed() => break, - result = ent.metadata() => { - let url = ent.url(); + result = ent.file() => { _ = tx.send(match result { - Ok(cha) => File::from_follow(url, cha).await, - Err(_) => File::from_dummy(url, ent.file_type().await.ok()) + Ok(file) => file, + Err(_) => File::from_dummy(ent.url(), ent.file_type().await.ok()), }); } } @@ -48,10 +47,9 @@ impl VfsEntries for Entries { async fn go(entries: &[DirEntry]) -> Vec { let mut files = Vec::with_capacity(entries.len()); for ent in entries { - let url = ent.url(); - files.push(match ent.metadata().await { - Ok(cha) => File::from_follow(url, cha).await, - Err(_) => File::from_dummy(url, ent.file_type().await.ok()), + files.push(match ent.file().await { + Ok(file) => file, + Err(_) => File::from_dummy(ent.url(), ent.file_type().await.ok()), }); } files @@ -66,14 +64,15 @@ impl VfsEntries for Entries { ) } - async fn assert_stale(dir: &UrlBuf, cha: Cha) -> Option { - use std::io::ErrorKind; - match Cha::from_url(dir).await { - Ok(c) if !c.is_dir() => FilesOp::issue_error(dir, ErrorKind::NotADirectory).await, - Ok(c) if c.hits(cha) && !PARTITIONS.read().timeless(cha) => {} - Ok(c) => return Some(c), - Err(e) => FilesOp::issue_error(dir, e).await, + async fn revalidate(old: &File) -> io::Result> { + match engine::revalidate(old).await? { + Some(new) if new.url != old.url => { + Err(io::Error::new(io::ErrorKind::InvalidData, "revalidated file URL changed")) + } + Some(new) if !new.is_dir() => Err(io::ErrorKind::NotADirectory.into()), + Some(new) => Ok(Some(new)), + None if PARTITIONS.read().timeless(old.cha) => Ok(Some(old.clone())), + None => Ok(None), } - None } } diff --git a/yazi-vfs/src/file.rs b/yazi-vfs/src/file.rs index 8b4a7d72..9a45d62e 100644 --- a/yazi-vfs/src/file.rs +++ b/yazi-vfs/src/file.rs @@ -1,40 +1,30 @@ use std::io; -use yazi_fs::{cha::Cha, file::File}; +use yazi_fs::{cha::Cha, file::{File, FileExtra}}; use yazi_shared::url::{UrlBuf, UrlCow}; use crate::{VfsCha, engine}; pub trait VfsFile: Sized { - fn new<'a>(url: impl Into>) -> impl Future>; - fn maybe_new<'a>(url: impl Into>) -> impl Future>>; fn from_follow(url: UrlBuf, cha: Cha) -> impl Future; } impl VfsFile for File { - async fn new<'a>(url: impl Into>) -> io::Result { - let url = url.into(); - let cha = engine::symlink_metadata(&url).await?; - Ok(Self::from_follow(url.into_owned(), cha).await) - } - async fn maybe_new<'a>(url: impl Into>) -> io::Result> { let url = url.into(); - let cha = match engine::symlink_metadata(&url).await { - Ok(cha) => cha, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Ok(match engine::file(&url).await { + Ok(file) => Some(file), + Err(e) if e.kind() == io::ErrorKind::NotFound => None, Err(e) => return Err(e), - }; - Ok(Some(Self::from_follow(url.into_owned(), cha).await)) + }) } async fn from_follow(url: UrlBuf, cha: Cha) -> Self { let link_to = if cha.is_link() { engine::read_link(&url).await.ok() } else { None }; - let cha = Cha::from_follow(&url, cha).await; - Self { url, cha, link_to } + Self { url, cha, extra: FileExtra::new(link_to, None) } } } diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index 733fc5b5..79e34f4d 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -1,6 +1,7 @@ use std::io::{self}; use yazi_shared::{strand::{StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shim::OptionExt; use crate::engine; @@ -23,7 +24,7 @@ pub async fn unique_file(u: UrlBuf, is_dir: bool) -> io::Result { } async fn _unique_file(mut url: UrlBuf, is_dir: bool) -> io::Result { - let Some(stem) = url.stem().map(|s| s.to_owned()) else { + let Some(stem) = url.stem().owned() else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index fb81f98b..18174963 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,5 +1,5 @@ yazi_macro::mod_pub!(engine); -yazi_macro::mod_flat!(cha entries file fns op); +yazi_macro::mod_flat!(cha entries file fns stamp); pub fn init() { engine::init(); } diff --git a/yazi-vfs/src/op.rs b/yazi-vfs/src/op.rs deleted file mode 100644 index e317e0a5..00000000 --- a/yazi-vfs/src/op.rs +++ /dev/null @@ -1,21 +0,0 @@ -use yazi_fs::FilesOp; -use yazi_shared::url::{UrlBuf, UrlLike}; - -use crate::maybe_exists; - -pub trait VfsFilesOp { - fn issue_error(cwd: &UrlBuf, kind: impl Into) -> impl Future; -} - -impl VfsFilesOp for FilesOp { - async fn issue_error(cwd: &UrlBuf, err: impl Into) { - let err = err.into(); - if err.kind() != std::io::ErrorKind::NotFound { - Self::IOErr(cwd.clone(), err).emit(); - } else if maybe_exists(cwd).await { - Self::IOErr(cwd.clone(), err).emit(); - } else if let Some((p, n)) = cwd.pair() { - Self::Deleting(p.into(), [n.into()].into()).emit(); - } - } -} diff --git a/yazi-vfs/src/stamp.rs b/yazi-vfs/src/stamp.rs new file mode 100644 index 00000000..dbf60a20 --- /dev/null +++ b/yazi-vfs/src/stamp.rs @@ -0,0 +1,108 @@ +use std::{io, path::Path, str}; + +use yazi_fs::{FsAuth, FsHash128, FsUrl, cha::Cha, engine::{Engine, local::Local}}; +use yazi_shared::{strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; + +pub struct Stamp(Vec); + +impl Stamp { + const SIG_LEN: usize = 26; + + pub async fn read(url: U) -> io::Result + where + U: AsUrl, + { + let path = + url.as_url().stamp_entry().ok_or_else(|| io::Error::other("Cannot determine cache stamp"))?; + + Self::read_at(&path).await + } + + async fn read_at(path: &Path) -> io::Result { + let data = Local::regular(path) + .read() + .await + .map_err(|e| io::Error::new(e.kind(), format!("Cannot read cache stamp: {e}")))?; + + Self::try_from(data) + .map_err(|e| io::Error::new(e.kind(), format!("Cannot parse cache stamp: {e}"))) + } + + pub async fn resolve(dir: U, key: S) -> io::Result + where + U: AsUrl, + S: AsStrand, + { + let dir = dir.as_url(); + let key = key.as_strand(); + + let mut path = + dir.auth().stamp_root().ok_or_else(|| io::Error::other("Cannot determine stamp root"))?; + path.push(key.as_os()?); + + let stamp = Self::read_at(&path).await?; + let name = StrandCow::with(dir.kind(), stamp.name()).map_err(io::Error::other)?; + + let url = dir.try_join(name)?; + if url.hash_u128_str(&mut [0; Self::SIG_LEN]).as_bytes() != key.encoded_bytes() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Cache stamp does not match entry")); + } + + Ok(url) + } + + pub async fn write(cha: Cha, url: Url<'_>) -> io::Result<()> { + let path = url.stamp_entry().ok_or_else(|| io::Error::other("Cannot determine cache stamp"))?; + let data = Self::encode(cha, url)?; + + Local::regular(&path) + .write(data) + .await + .map_err(|e| io::Error::new(e.kind(), format!("Cannot write cache stamp: {e}"))) + } + + fn encode(cha: Cha, url: Url) -> io::Result> { + let name = url.name().ok_or_else(|| io::Error::other("URL has no filename"))?; + + let mut buf = Vec::with_capacity(Self::SIG_LEN + name.len()); + buf.extend_from_slice(cha.hash_u128_str(&mut [0; Self::SIG_LEN]).as_bytes()); + buf.extend_from_slice(name.encoded_bytes()); + + Ok(buf) + } + + pub fn validate(&self, cha: Cha, url: Url) -> io::Result<()> { + let name = url.name().ok_or_else(|| io::Error::other("URL has no filename"))?; + if self.name() != name.encoded_bytes() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Cache stamp does not match target")); + } + + if self.sig() != cha.hash_u128_str(&mut [0; Self::SIG_LEN]) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Remote file has changed since last download", + )); + } + Ok(()) + } + + #[inline] + pub fn sig(&self) -> &str { unsafe { str::from_utf8_unchecked(&self.0[..Self::SIG_LEN]) } } + + #[inline] + pub fn name(&self) -> &[u8] { &self.0[Self::SIG_LEN..] } +} + +impl TryFrom> for Stamp { + type Error = io::Error; + + fn try_from(value: Vec) -> Result { + let (sig, _) = value + .split_at_checked(Self::SIG_LEN) + .filter(|(_, n)| !n.is_empty()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid cache stamp"))?; + + str::from_utf8(sig).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + Ok(Self(value)) + } +} diff --git a/yazi-watcher/Cargo.toml b/yazi-watcher/Cargo.toml index df56f6ef..f87d3e57 100644 --- a/yazi-watcher/Cargo.toml +++ b/yazi-watcher/Cargo.toml @@ -24,9 +24,9 @@ yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } # External dependencies futures = { workspace = true } hashbrown = { workspace = true } +indexmap = { workspace = true } notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] } parking_lot = { workspace = true } -percent-encoding = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } diff --git a/yazi-watcher/src/backend.rs b/yazi-watcher/src/backend.rs index ec83fdf7..c8e5b56a 100644 --- a/yazi-watcher/src/backend.rs +++ b/yazi-watcher/src/backend.rs @@ -1,11 +1,11 @@ use notify::Result; use tokio::sync::mpsc; -use crate::{Reporter, WATCHED, Watchee, local::{self, LINKED, Linked}, remote}; +use crate::{Reporter, WATCHED, Watchee, local::{self, LINKED, Linked}, r#virtual}; pub(crate) struct Backend { local: local::Local, - remote: remote::Remote, + r#virtual: r#virtual::Virtual, pub(super) reporter: Reporter, } @@ -19,12 +19,12 @@ impl Backend { }); let (local_tx, local_rx) = mpsc::unbounded_channel(); - let (remote_tx, remote_rx) = mpsc::unbounded_channel(); - let reporter = Reporter { local_tx, remote_tx }; + let (virtual_tx, virtual_rx) = mpsc::unbounded_channel(); + let reporter = Reporter { local_tx, virtual_tx }; Self { local: local::Local::serve(local_rx, reporter.clone()), - remote: remote::Remote::serve(remote_rx, reporter.clone()), + r#virtual: r#virtual::Virtual::serve(virtual_rx), reporter, } } @@ -32,14 +32,14 @@ impl Backend { pub(super) fn watch(&mut self, watchee: &mut Watchee) -> Result<()> { match watchee { Watchee::Local(..) => self.local.watch(watchee), - Watchee::Remote(_) => self.remote.watch(watchee), + Watchee::Virtual(_) => self.r#virtual.watch(watchee), } } pub(super) fn unwatch(&mut self, watchee: &Watchee) -> Result<()> { match watchee { Watchee::Local(..) => self.local.unwatch(watchee), - Watchee::Remote(_) => self.remote.unwatch(watchee), + Watchee::Virtual(_) => self.r#virtual.unwatch(watchee), } } diff --git a/yazi-watcher/src/lib.rs b/yazi-watcher/src/lib.rs index 4ba56039..a28272fe 100644 --- a/yazi-watcher/src/lib.rs +++ b/yazi-watcher/src/lib.rs @@ -1,6 +1,6 @@ -yazi_macro::mod_pub!(local remote); +yazi_macro::mod_pub!(local r#virtual); -yazi_macro::mod_flat!(backend proxy reporter watched watchee watcher); +yazi_macro::mod_flat!(backend proxy refresher reporter watched watchee watcher); pub static WATCHED: yazi_shim::cell::RoCell> = yazi_shim::cell::RoCell::new(); diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 131e0af7..8f296f23 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -1,13 +1,12 @@ -use std::{path::Path, time::Duration}; +use std::{io, path::Path, time::Duration}; use hashbrown::HashSet; use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Result, Watcher}; use tokio::{pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tracing::error; -use yazi_fs::{FilesOp, engine::{self, Engine}, file::File, mounts::PARTITIONS}; +use yazi_fs::{FilesOp, engine::{self, Engine}, mounts::PARTITIONS}; use yazi_shared::url::{UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; use crate::{Reporter, WATCHER, Watchee}; @@ -96,16 +95,23 @@ impl Local { let _permit = WATCHER.acquire().await.unwrap(); let mut ops = Vec::with_capacity(urls.len()); - for u in urls { - let Some((parent, key)) = u.pair2() else { continue }; - let Ok(file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); - continue; + for url in urls { + let Some(path) = url.as_local() else { continue }; + let Some((parent, key)) = url.pair2() else { continue }; + + let file = match engine::local::Local::regular(path).file().await { + Ok(file) => file, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); + continue; + } + Err(e) => { + tracing::error!("Failed to update {url:?}: {e:?}"); + continue; + } }; - if let Some(p) = file.url.as_local() - && !engine::local::match_name_case(p).await - { + if !engine::local::match_name_case(path).await { ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; } diff --git a/yazi-watcher/src/refresher.rs b/yazi-watcher/src/refresher.rs new file mode 100644 index 00000000..a73dad5a --- /dev/null +++ b/yazi-watcher/src/refresher.rs @@ -0,0 +1,183 @@ +use std::{io, ops::Deref, time::{Duration, Instant}}; + +use hashbrown::{HashMap, hash_map::RawEntryMut}; +use indexmap::IndexSet; +use tokio::sync::mpsc; +use yazi_fs::{Entries, FilesOp, file::{File, FileCov}}; +use yazi_shared::url::{UrlBuf, UrlCov, UrlLike, UrlMapExt}; +use yazi_vfs::VfsEntries; + +#[derive(Clone)] +pub struct Refresher { + tx: mpsc::UnboundedSender, +} + +enum Op { + Sync(IndexSet), + Refresh(IndexSet), + Load(File), + Touch(IndexSet), + Done(Entry, io::Result>>), +} + +impl Refresher { + pub(super) fn serve() -> Self { + let (tx, mut rx) = mpsc::unbounded_channel(); + let me = Self { tx }; + + let me_ = me.clone(); + tokio::spawn(async move { + let mut entries = HashMap::new(); + let mut interval = tokio::time::interval(Duration::from_secs(2)); + + loop { + tokio::select! { + Some(op) = rx.recv() => me_.handle(op, &mut entries).await, + _ = interval.tick() => { + for (_, entry) in entries.iter_mut().filter(|(u, _)| u.kind().is_virtual()) { + entry.dirty = true; + me_.spawn(entry); + } + } + } + } + }); + + me + } + + async fn handle(&self, op: Op, entries: &mut HashMap) { + match op { + Op::Sync(files) => { + entries.retain(|url, _| files.contains(&UrlCov::new(url))); + for file in files { + entries.get_or_insert_with(file.0, |file| Entry { file, ..Default::default() }); + } + } + Op::Refresh(files) => { + for file in files.into_iter().map(|file| file.0) { + let entry = match entries.raw_entry_mut().from_key(&file.url) { + RawEntryMut::Occupied(mut oe) => { + oe.get_mut().file = file; + oe.into_mut() + } + RawEntryMut::Vacant(ve) => { + ve.insert(file.url.to_owned(), Entry { file, ..Default::default() }).1 + } + }; + + (entry.dirty, entry.report) = (true, true); + self.spawn(entry); + } + } + Op::Load(file) => { + let entry = entries.get_or_insert_with(file, |file| Entry { file, ..Default::default() }); + (entry.dirty, entry.report, entry.force) = (true, true, true); + self.spawn(entry); + } + Op::Touch(urls) => { + for url in urls { + let Some(entry) = entries.get_mut(&url) else { continue }; + entry.dirty = true; + self.spawn(entry); + } + } + Op::Done(prev, result) => { + let Some(entry) = entries.get_mut(&prev.url) else { return }; + if entry.busy != prev.busy { + return; + } + + match result { + Ok(Some(files)) => { + entry.file = prev.file.clone(); + FilesOp::Full(prev.file, files).emit(); + } + Ok(None) => {} + Err(e) if e.kind() == io::ErrorKind::NotFound => { + if let Some((p, n)) = prev.url.pair2() { + FilesOp::Deleting(p.into(), [n.into()].into()).emit(); + } + } + Err(e) if prev.report => { + FilesOp::IOErr(prev.file.url, e.into()).emit(); + } + Err(e) => tracing::debug!("Failed to refresh {:?}: {e:?}", prev.url), + } + + entry.busy = None; + self.spawn(entry); // A new request may have arrived while this entry was busy. + } + } + } + + fn spawn(&self, entry: &mut Entry) { + if entry.busy.is_some() || !entry.dirty { + return; + } + + let (tx, mut prev) = (self.tx.clone(), entry.turn()); + tokio::spawn(async move { + let result = async { + Ok(if prev.force { + Some(Entries::from_dir_bulk(&prev.file.url).await?) + } else if let Some(file) = Entries::revalidate(&prev.file).await? { + prev.file = file; + Some(Entries::from_dir_bulk(&prev.url).await?) + } else { + None + }) + } + .await; + tx.send(Op::Done(prev, result)).ok(); + }); + } +} + +impl Refresher { + pub(super) fn sync(&self, files: IndexSet) { self.tx.send(Op::Sync(files)).ok(); } + + pub fn refresh(&self, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let entries = files.into_iter().map(|file| FileCov(file.into())).collect(); + self.tx.send(Op::Refresh(entries)).ok(); + } + + pub fn load(&self, file: impl Into) { self.tx.send(Op::Load(file.into())).ok(); } + + pub fn touch(&self, urls: I) + where + I: IntoIterator, + { + self.tx.send(Op::Touch(urls.into_iter().collect())).ok(); + } +} + +// --- Entry +#[derive(Clone, Default)] +struct Entry { + file: File, + busy: Option, + dirty: bool, + report: bool, + force: bool, +} + +impl Deref for Entry { + type Target = File; + + fn deref(&self) -> &Self::Target { &self.file } +} + +impl Entry { + fn turn(&mut self) -> Self { + self.busy = Some(Instant::now()); + let me = self.clone(); + + (self.dirty, self.report, self.force) = (false, false, false); + me + } +} diff --git a/yazi-watcher/src/remote/mod.rs b/yazi-watcher/src/remote/mod.rs deleted file mode 100644 index 362fa369..00000000 --- a/yazi-watcher/src/remote/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(remote); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/remote/remote.rs deleted file mode 100644 index 6a94b9d1..00000000 --- a/yazi-watcher/src/remote/remote.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::time::{Duration, SystemTime}; - -use hashbrown::HashMap; -use notify::Result; -use tokio::{pin, sync::mpsc::UnboundedReceiver}; -use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; -use yazi_fs::{FilesOp, file::File}; -use yazi_shared::url::{UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; - -use crate::{MgrProxy, Reporter, WATCHER, Watchee}; - -pub(crate) struct Remote; - -impl Remote { - pub(crate) fn serve(rx: UnboundedReceiver<(UrlBuf, bool)>, _reporter: Reporter) -> Self { - tokio::spawn(Self::changed(rx)); - - Self - } - - pub(crate) fn watch(&mut self, _watchee: &mut Watchee) -> Result<()> { Ok(()) } - - pub(crate) fn unwatch(&mut self, _watchee: &Watchee) -> Result<()> { Ok(()) } - - async fn changed(rx: UnboundedReceiver<(UrlBuf, bool)>) { - let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250)); - pin!(rx); - - while let Some(chunk) = rx.next().await { - let mut urls = HashMap::with_capacity(chunk.len()); - for (u, upload) in chunk { - urls.entry(u).and_modify(|b| *b |= upload).or_insert(upload); - } - - let _permit = WATCHER.acquire().await.unwrap(); - - let mut ops = Vec::with_capacity(urls.len()); - let mut ups = Vec::with_capacity(urls.len()); - - for (u, upload) in urls { - let Some((parent, key)) = u.pair2() else { continue }; - let Ok(mut file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); - continue; - }; - - let is_file = file.is_file(); - file.cha.ctime = Some(SystemTime::now()); - - ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); - if upload && is_file { - ups.push(u); - } - } - - FilesOp::mutate(ops); - MgrProxy::upload(ups); - } - } -} diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 97718f8d..23b04251 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,15 +1,12 @@ -use std::borrow::Cow; - -use percent_encoding::percent_decode; use tokio::sync::mpsc; use yazi_shared::{auth::AuthKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; -use crate::{WATCHED, local::LINKED}; +use crate::{WATCHED, local::LINKED, r#virtual::VirtualReport}; #[derive(Clone)] pub(crate) struct Reporter { - pub(super) local_tx: mpsc::UnboundedSender, - pub(super) remote_tx: mpsc::UnboundedSender<(UrlBuf, bool)>, + pub(super) local_tx: mpsc::UnboundedSender, + pub(super) virtual_tx: mpsc::UnboundedSender, } impl Reporter { @@ -21,8 +18,9 @@ impl Reporter { for url in urls.into_iter().map(Into::into) { match url.as_url().kind() { AuthKind::Regular | AuthKind::Search => self.report_local(url), - AuthKind::Mount | AuthKind::Hub => {} // TODO: mounted VFS cache invalidation - AuthKind::Scope | AuthKind::Sftp => self.report_remote(url), + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { + self.report_virtual(url) + } } } } @@ -47,21 +45,18 @@ impl Reporter { // Virtual caches let Some(dir) = watched.find_by_cache(parent.loc()) else { continue }; - let Some(name) = url.name() else { continue }; - if let Ok(u) = dir.try_join(Cow::from(percent_decode(name.encoded_bytes()))) { - self.remote_tx.send((u, true)).ok(); - } - self.remote_tx.send((dir, false)).ok(); + let Some(key) = url.name() else { continue }; + self.virtual_tx.send(VirtualReport::Cache(dir, key.to_owned())).ok(); } } - fn report_remote(&self, url: UrlCow) { + fn report_virtual(&self, url: UrlCow) { let Some(parent) = url.parent() else { return }; if !WATCHED.read().contains_url(parent) { return; } - self.remote_tx.send((parent.to_owned(), false)).ok(); - self.remote_tx.send((url.into_owned(), false)).ok(); + self.virtual_tx.send(VirtualReport::Url(parent.to_owned())).ok(); + self.virtual_tx.send(VirtualReport::Url(url.into_owned())).ok(); } } diff --git a/yazi-watcher/src/virtual/mod.rs b/yazi-watcher/src/virtual/mod.rs new file mode 100644 index 00000000..813ceb9f --- /dev/null +++ b/yazi-watcher/src/virtual/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(r#virtual); diff --git a/yazi-watcher/src/virtual/virtual.rs b/yazi-watcher/src/virtual/virtual.rs new file mode 100644 index 00000000..f67a46a9 --- /dev/null +++ b/yazi-watcher/src/virtual/virtual.rs @@ -0,0 +1,85 @@ +use std::{io, time::{Duration, SystemTime}}; + +use hashbrown::{HashMap, HashSet}; +use notify::Result; +use tokio::{pin, sync::mpsc::UnboundedReceiver}; +use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use yazi_fs::FilesOp; +use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}}; +use yazi_vfs::{Stamp, engine}; + +use crate::{MgrProxy, WATCHER, Watchee}; + +pub(crate) struct Virtual; + +#[derive(Hash, PartialEq, Eq)] +pub(crate) enum VirtualReport { + Url(UrlBuf), + Cache(UrlBuf, StrandBuf), +} + +impl Virtual { + pub(crate) fn serve(rx: UnboundedReceiver) -> Self { + tokio::spawn(Self::changed(rx)); + + Self + } + + pub(crate) fn watch(&mut self, _watchee: &mut Watchee) -> Result<()> { Ok(()) } + + pub(crate) fn unwatch(&mut self, _watchee: &Watchee) -> Result<()> { Ok(()) } + + async fn changed(rx: UnboundedReceiver) { + let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250)); + pin!(rx); + + while let Some(chunk) = rx.next().await { + let reports: HashSet<_> = chunk.into_iter().collect(); + + let mut urls = HashMap::with_capacity(reports.len()); + for report in reports { + match report { + VirtualReport::Url(url) => _ = urls.entry(url).or_insert(false), + VirtualReport::Cache(dir, key) if let Ok(file) = Stamp::resolve(&dir, &key).await => { + urls.insert(file, true); + urls.entry(dir).or_insert(false); + } + VirtualReport::Cache(..) => {} + } + } + + let _permit = WATCHER.acquire().await.unwrap(); + + let mut ops = Vec::with_capacity(urls.len()); + let mut ups = Vec::with_capacity(urls.len()); + + for (url, upload) in urls { + let Some((parent, key)) = url.pair2() else { continue }; + + let mut file = match engine::file(&url).await { + Ok(file) => file, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); + continue; + } + Err(e) => { + tracing::debug!("Failed to update {url:?}: {e:?}"); + continue; + } + }; + + if upload && file.is_file() { + file.cha.ctime = Some(SystemTime::now()); + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); + ups.push(url); + continue; + } + + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); + } + + FilesOp::mutate(ops); + MgrProxy::upload(ups); + } + } +} diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index 2c035378..bb6024f2 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -1,66 +1,69 @@ -use std::{ops::{Deref, DerefMut}, path::Path}; +use std::{ops::Deref, path::{Path, PathBuf}}; -use hashbrown::HashSet; -use yazi_fs::{Xdg, path::PercentEncoding}; -use yazi_shared::{auth::{Auth, AuthKind}, path::{Component, PathBufDyn, PathDyn, PathLike}, url::{AsUrl, UrlBuf}}; +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexSet; +use yazi_fs::FsUrl; +use yazi_shared::{path::PathDyn, url::{AsUrl, UrlBuf}}; use crate::Watchee; #[derive(Debug, Default)] -pub struct Watched(HashSet>); +pub struct Watched { + urls: HashSet>, + caches: HashMap, +} impl Deref for Watched { type Target = HashSet>; - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Watched { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } + fn deref(&self) -> &Self::Target { &self.urls } } impl Watched { + pub(super) fn insert(&mut self, watchee: Watchee<'static>) { + let url = watchee.as_url(); + if let Some(cache) = url.cache_bucket() { + self.caches.insert(cache, url.into()); + } + self.urls.insert(watchee); + } + + pub(super) fn remove(&mut self, watchee: &Watchee<'static>) -> bool { + if !self.urls.remove(watchee) { + return false; + } + if let Some(cache) = watchee.as_url().cache_bucket() { + self.caches.remove(&cache); + } + true + } + + pub(super) fn difference<'a>( + &'a self, + other: &'a IndexSet>, + ) -> impl Iterator> { + self.urls.iter().filter(move |&watchee| !other.contains(watchee)) + } + pub(super) fn contains_url(&self, url: impl AsUrl) -> bool { let url = url.as_url(); if url.as_local().is_some() { - self.0.contains(&Watchee::Local(url.into(), false)) - || self.0.contains(&Watchee::Local(url.into(), true)) + self.urls.contains(&Watchee::Local(url.into(), false)) + || self.urls.contains(&Watchee::Local(url.into(), true)) } else { - self.0.contains(&Watchee::Remote(url.into())) + self.urls.contains(&Watchee::Virtual(url.into())) } } pub(super) fn contains_path(&self, path: &Path) -> bool { - self.0.iter().any(|watchee| watchee.as_url().as_local() == Some(path)) + self.urls.iter().any(|watchee| watchee.as_url().as_local() == Some(path)) } pub(super) fn paths(&self) -> impl Iterator { - self.0.iter().filter_map(|watchee| watchee.as_url().as_local()) + self.urls.iter().filter_map(|watchee| watchee.as_url().as_local()) } pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option { - let mut it = cache.try_strip_prefix(Xdg::temp_dir()).ok()?.components(); - - // Parse authority - let cache = it.next()?.as_normal()?.to_str().ok()?; - let auth = Auth::parse_cache(cache).ok()?; - - // Parse path - let (path, abs) = - if let Ok(p) = it.path().try_strip_prefix(".%2F") { (p, false) } else { (it.path(), true) }; - let path = path.percent_decode(auth.kind).ok()?; - let path = PathBufDyn::from_components( - auth.kind, - abs.then_some(Component::RootDir).into_iter().chain(path.components()), - ) - .ok()?; - - let url = match auth.kind { - AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth }, - AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth }, - AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth }, - AuthKind::Regular | AuthKind::Search | AuthKind::Hub => return None, - }; - if self.contains_url(&url) { Some(url) } else { None } + self.caches.get(cache.as_os().ok()?).cloned() } } diff --git a/yazi-watcher/src/watchee.rs b/yazi-watcher/src/watchee.rs index 47b8c091..2fa88668 100644 --- a/yazi-watcher/src/watchee.rs +++ b/yazi-watcher/src/watchee.rs @@ -7,14 +7,14 @@ use crate::local::Local; #[derive(Debug, Eq, Hash, PartialEq)] pub enum Watchee<'a> { Local(UrlCow<'a>, bool), - Remote(UrlCow<'a>), + Virtual(UrlCow<'a>), } impl AsUrl for Watchee<'_> { fn as_url(&self) -> Url<'_> { match self { Self::Local(url, _) => url.as_url(), - Self::Remote(url) => url.as_url(), + Self::Virtual(url) => url.as_url(), } } } @@ -23,14 +23,14 @@ impl<'a> Watchee<'a> { pub(super) fn as_local(&self) -> Option<(&Path, bool)> { Some(match self { Self::Local(url, alt) => (url.as_local()?, *alt), - Self::Remote(_) => None?, + Self::Virtual(_) => None?, }) } pub(super) fn as_local_mut(&mut self) -> Option<(&Path, &mut bool)> { Some(match self { Self::Local(url, alt) => (url.as_local()?, alt), - Self::Remote(_) => None?, + Self::Virtual(_) => None?, }) } @@ -43,14 +43,14 @@ impl<'a> Watchee<'a> { let b = Local::soundless(path).await; Self::Local(url, b) } else { - Self::Remote(url) + Self::Virtual(url) } } pub(super) fn to_static(&self) -> Watchee<'static> { match self { Self::Local(url, alt) => Watchee::Local(url.to_owned().into(), *alt), - Self::Remote(url) => Watchee::Remote(url.to_owned().into()), + Self::Virtual(url) => Watchee::Virtual(url.to_owned().into()), } } } diff --git a/yazi-watcher/src/watcher.rs b/yazi-watcher/src/watcher.rs index a48d2044..da4c5f13 100644 --- a/yazi-watcher/src/watcher.rs +++ b/yazi-watcher/src/watcher.rs @@ -1,45 +1,50 @@ use futures::{StreamExt, stream}; -use hashbrown::HashSet; +use indexmap::IndexSet; use tracing::error; -use yazi_fs::FsUrl; +use yazi_fs::{FsUrl, file::File}; use yazi_shared::{LastValue, url::{UrlBuf, UrlCow, UrlLike}}; -use crate::{Reporter, WATCHED, Watchee, backend::Backend}; +use crate::{Refresher, Reporter, WATCHED, Watchee, backend::Backend}; pub struct Watcher { - last: LastValue>, - reporter: Reporter, + last: LastValue>, + reporter: Reporter, + pub refresher: Refresher, } impl Watcher { pub fn serve() -> Self { let last = LastValue::default(); + let refresher = Refresher::serve(); let backend = Backend::serve(); let reporter = backend.reporter.clone(); - tokio::spawn(Self::watched(last.clone(), backend)); - Self { last, reporter } + tokio::spawn(Self::run(last.clone(), backend)); + Self { last, reporter, refresher } } - pub fn watch<'a, I>(&mut self, urls: I) + pub fn watch(&mut self, files: I) where I: IntoIterator, - I::Item: Into>, + I::Item: Into, { - let it = urls.into_iter(); - let mut urls = HashSet::with_capacity(it.size_hint().0); + let it = files.into_iter(); + let mut urls = IndexSet::with_capacity(it.size_hint().0); + let mut files = IndexSet::with_capacity(it.size_hint().0); - for url in it.map(Into::into) { - if !url.is_absolute() { + for file in it.map(Into::into) { + if !file.url.is_absolute() { continue; - } else if let Some(cache) = url.cache() { + } else if let Some(cache) = file.url.cache_bucket() { urls.insert(cache.into()); } - urls.insert(url.into_owned()); + urls.insert(file.url.clone()); + files.insert(file.into()); } self.last.set(urls); + self.refresher.sync(files); } pub fn report<'a, I>(&self, urls: I) @@ -50,7 +55,7 @@ impl Watcher { self.reporter.report(urls); } - async fn watched(last: LastValue>, mut backend: Backend) { + async fn run(last: LastValue>, mut backend: Backend) { loop { let (to_unwatch, to_watch) = Self::diff(last.get().await).await; @@ -61,8 +66,8 @@ impl Watcher { } } - async fn diff(urls: HashSet) -> (Vec>, HashSet>) { - let mut new: HashSet<_> = stream::iter(urls).then(Watchee::new).collect().await; + async fn diff(urls: IndexSet) -> (Vec>, IndexSet>) { + let mut new: IndexSet<_> = stream::iter(urls).then(Watchee::new).collect().await; let old = WATCHED.read(); let to_unwatch = old.difference(&new).map(Watchee::to_static).collect(); @@ -74,7 +79,7 @@ impl Watcher { async fn sync( mut backend: Backend, to_unwatch: Vec>, - to_watch: HashSet>, + to_watch: IndexSet>, ) -> Backend { tokio::task::spawn_blocking(move || { for watchee in to_unwatch { @@ -85,7 +90,7 @@ impl Watcher { } for mut watchee in to_watch { match backend.watch(&mut watchee) { - Ok(()) => _ = WATCHED.write().insert(watchee), + Ok(()) => WATCHED.write().insert(watchee), Err(e) if matches!(e.kind, notify::ErrorKind::PathNotFound) => {} Err(e) => error!("Watch failed: {e:?}"), } From 2c3f174eb5ed9536aa090f2056c6af1ef30323c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sat, 25 Jul 2026 11:39:19 +0800 Subject: [PATCH 12/12] feat: allow restoring trashed items recursively (#4159) --- AGENTS.md | 45 +++ CLAUDE.md | 1 + Cargo.lock | 151 +++++--- Cargo.toml | 16 +- cspell.json | 2 +- yazi-adapter/Cargo.toml | 2 +- yazi-codegen/Cargo.toml | 2 +- yazi-fs/Cargo.toml | 8 +- yazi-fs/build.rs | 13 + yazi-fs/src/cha/cha.rs | 12 +- yazi-fs/src/cha/kind.rs | 25 +- yazi-fs/src/cha/lua.rs | 1 + yazi-fs/src/engine/local/calculator.rs | 26 +- yazi-fs/src/engine/local/clear.rs | 126 +++++++ yazi-fs/src/engine/local/local.rs | 6 + yazi-fs/src/engine/local/mod.rs | 2 +- yazi-fs/src/engine/traits.rs | 37 +- yazi-fs/src/trash/common.rs | 18 + yazi-fs/src/trash/entries.rs | 38 ++ yazi-fs/src/trash/entry.rs | 107 ++++-- yazi-fs/src/trash/freedesktop.rs | 171 --------- yazi-fs/src/trash/freedesktop/mod.rs | 1 + yazi-fs/src/trash/freedesktop/trash.rs | 150 ++++++++ yazi-fs/src/trash/freedesktop/trash_info.rs | 96 +++++ yazi-fs/src/trash/lua.rs | 88 +++-- yazi-fs/src/trash/macos.rs | 188 ---------- yazi-fs/src/trash/macos/ds_store.rs | 57 +++ yazi-fs/src/trash/macos/mod.rs | 1 + yazi-fs/src/trash/macos/trash.rs | 133 +++++++ yazi-fs/src/trash/mod.rs | 11 +- yazi-fs/src/trash/node.rs | 49 --- yazi-fs/src/trash/nodes.rs | 36 -- yazi-fs/src/trash/traits.rs | 27 +- yazi-fs/src/trash/trash_id.rs | 59 ++++ yazi-fs/src/trash/unsupported/mod.rs | 1 + .../{unsupported.rs => unsupported/trash.rs} | 24 +- yazi-fs/src/trash/windows.rs | 330 ------------------ yazi-fs/src/trash/windows/mod.rs | 1 + yazi-fs/src/trash/windows/shell_item.rs | 96 +++++ yazi-fs/src/trash/windows/trash.rs | 199 +++++++++++ yazi-fs/src/trash/windows/trash_sig.rs | 68 ++++ yazi-plugin/preset/plugins/trash.lua | 157 ++++++--- yazi-scheduler/src/file/file.rs | 34 +- yazi-scheduler/src/file/transaction.rs | 2 +- yazi-scheduler/src/file/traverse.rs | 6 +- yazi-shared/src/auth/auth.rs | 20 +- yazi-shared/src/path/buf.rs | 4 + yazi-shared/src/strand/strand.rs | 11 +- yazi-shim/Cargo.toml | 1 - yazi-shim/src/base64.rs | 8 - yazi-shim/src/lib.rs | 2 +- yazi-shim/src/path/mod.rs | 2 +- yazi-shim/src/path/traits.rs | 11 + yazi-term/src/event/dnd.rs | 5 +- yazi-tty/src/sequence/dnd.rs | 7 +- yazi-tui/src/raterm.rs | 3 +- yazi-vfs/src/engine/calculator.rs | 8 +- yazi-vfs/src/engine/engines.rs | 8 + 58 files changed, 1651 insertions(+), 1062 deletions(-) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 yazi-fs/build.rs create mode 100644 yazi-fs/src/engine/local/clear.rs create mode 100644 yazi-fs/src/trash/common.rs create mode 100644 yazi-fs/src/trash/entries.rs delete mode 100644 yazi-fs/src/trash/freedesktop.rs create mode 100644 yazi-fs/src/trash/freedesktop/mod.rs create mode 100644 yazi-fs/src/trash/freedesktop/trash.rs create mode 100644 yazi-fs/src/trash/freedesktop/trash_info.rs delete mode 100644 yazi-fs/src/trash/macos.rs create mode 100644 yazi-fs/src/trash/macos/ds_store.rs create mode 100644 yazi-fs/src/trash/macos/mod.rs create mode 100644 yazi-fs/src/trash/macos/trash.rs delete mode 100644 yazi-fs/src/trash/node.rs delete mode 100644 yazi-fs/src/trash/nodes.rs create mode 100644 yazi-fs/src/trash/trash_id.rs create mode 100644 yazi-fs/src/trash/unsupported/mod.rs rename yazi-fs/src/trash/{unsupported.rs => unsupported/trash.rs} (57%) delete mode 100644 yazi-fs/src/trash/windows.rs create mode 100644 yazi-fs/src/trash/windows/mod.rs create mode 100644 yazi-fs/src/trash/windows/shell_item.rs create mode 100644 yazi-fs/src/trash/windows/trash.rs create mode 100644 yazi-fs/src/trash/windows/trash_sig.rs delete mode 100644 yazi-shim/src/base64.rs create mode 100644 yazi-shim/src/path/traits.rs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..67af2fac --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# AGENTS.md + +## Rules + +- Applies repo-wide. Keep changes scoped; do not create issues or pull requests, or post comments. + +## Project + +- Rust 2024 Cargo workspace containing all `yazi-*` crates; default members are `yazi-fm` (`yazi`) and `yazi-cli` (`ya`). + +## Style + +- Follow nearby code and use idiomatic Rust and Lua. Rust uses `snake_case` for modules, functions, and fields and `PascalCase` for types, traits, and variants. Lua uses PascalCase component tables, `local M` plugin modules, `snake_case` methods/locals, and `_name` private fields. +- Preserve established terms and type families: `Url`/`UrlBuf`/`UrlCow`, `PathDyn`/`PathBufDyn`/`PathCow`, `*Ref`, `*Arc`, `*Opt`, `*State`, `*Job`, `*Prog`, `File`, `Folder`, `Tab`, `Mgr`, and `Task`. Use `Url` for logical locations and `Path` for filesystem paths. +- Reuse established plugin and event names (`fetch`, `preload`, `peek`, `seek`, `spot`, `entry`, `setup`, `yank`, `hover`, and `select`) across Rust, Lua, and configuration. +- Use Rust prefixes (`as_`, `to_`, `into_`, `try_`, `is_`, `has_`) according to their usual semantics; prefer descriptive names. +- Name variables, modules, methods, and other symbols simply, elegantly, and expressively. Be creative while keeping names clear, consistent with established terminology, and idiomatic. +- When passing arguments, use the parameter's conversion traits directly (such as `Into<_>` or `AsRef<_>`); avoid eager conversions like `.to_string()`, `.to_owned()`, and `.as_ref()` unless ownership, type inference, or semantics require them. +- Prefer general-purpose traits and conversion APIs already provided by the codebase or its dependencies over manual construction or adapter closures; for example, use `into_lua()` where applicable. + +## Code Changes + +- Search and reuse first. For new features, extend existing infrastructure or data structures with general, reusable capabilities when that keeps the final code concise. +- For refactors, inspect the whole target module and its callers first. Look for duplicated work, redundant I/O, underpowered return values, one-use wrappers, and reusable cross-platform abstractions; implement high-confidence, behavior-preserving simplifications while preserving error, fallback, and platform semantics. +- Keep diffs minimal and avoid unrelated refactors. Prefer clear code over custom patterns or comments; comment only behavior the code cannot explain. +- Put reusable code in the lowest suitable shared layer; avoid unnecessary dependencies and allocations. Prefer borrowed values and existing wrappers. +- Use stable Rust APIs; nightly is formatting-only. Use only `pub`, `pub(super)`, and `pub(crate)`—never `pub(in ...)`. +- Keep async I/O non-blocking, preserve platform/fork behavior, and follow existing error boundaries with `?`. +- For renames or refactors, update all related variables, functions, parameters, modules, methods, types, derived types, exports, tests, configuration keys, documentation, and Lua bindings. +- Do not add or modify tests unless requested. + +## Validation + +- Prefer targeted debug checks; use multiple `-p` flags for affected crates before the whole workspace. + +```sh +cargo check -p +cargo test -p +cargo clippy -p +find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} + +stylua --color always --check . +``` + +- Use `cargo check` instead of `cargo build` unless artifacts are needed. Do not use `--release` unless requested; use `scripts/build.sh ` for release or cross-target packaging. +- Run relevant existing tests when needed, then inspect `git diff` and verify that only intended files changed. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8b243762..f4e8904f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,7 +145,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -156,7 +156,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -319,6 +319,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[package]] name = "base64ct" version = "1.8.3" @@ -573,9 +579,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -636,9 +642,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -687,14 +693,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -921,15 +927,16 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "5.0.0-rc.1" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", + "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -1076,7 +1083,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1122,9 +1129,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-rc.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1685663e23882cd8517dcbcb1c23a6ebff4433c22dfb681d760219b62cd1b849" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", @@ -1138,9 +1145,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" @@ -1226,7 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1660,7 +1667,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.56.0", ] [[package]] @@ -1690,7 +1697,7 @@ dependencies = [ "exr", "gif", "image-webp", - "moxcms", + "moxcms 0.8.1", "num-traits", "png", "qoi", @@ -1931,9 +1938,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libfuzzer-sys" @@ -2099,7 +2106,7 @@ dependencies = [ "module-lattice", "pkcs8", "rand_core 0.10.1", - "sha3", + "sha3 0.11.0", ] [[package]] @@ -2170,6 +2177,16 @@ dependencies = [ "pxfm", ] +[[package]] +name = "moxcms" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee701c0ffba6cc991c0fefda5ff5fca1bd2f2f4a928ffba9f825b9afceedbc3" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2245,7 +2262,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2436,9 +2453,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "p256" -version = "0.14.0-rc.15" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bb40a5099e2c38a09dd29321a7a7f045f165a54317679c7cdfb0cbaf8f6b1e" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", @@ -2449,9 +2466,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.14.0-rc.15" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492f329d7eb11d22dadc988626b9ea1f503b4ab043a8b1e4e2cc4ae45dabdd70" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ "ecdsa", "elliptic-curve", @@ -2463,9 +2480,9 @@ dependencies = [ [[package]] name = "p521" -version = "0.14.0-rc.15" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff42e4ace5424e3b6d7cb82514be89866b85af87015f80341e37dcc21a66ce6e" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" dependencies = [ "base16ct", "ecdsa", @@ -2662,7 +2679,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ - "base64", + "base64 0.22.1", "indexmap 2.14.0", "quick-xml", "serde", @@ -3114,7 +3131,7 @@ checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -3197,9 +3214,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.62.2" +version = "0.62.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6c3c1dd0a9ad3a9915a2f6e907d158f9a7e9ac32ddc772b003faafc027d9a8" +checksum = "b8b67b5a0d8068c89dcbe9d95df986af7a851d1f3c604525274c37468e60464f" dependencies = [ "aes", "bitflags", @@ -3253,7 +3270,7 @@ dependencies = [ "sec1", "sha1", "sha2", - "sha3", + "sha3 0.12.0", "signature", "spki", "ssh-encoding", @@ -3321,7 +3338,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3463,7 +3480,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -3494,7 +3511,7 @@ version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ - "base64", + "base64 0.22.1", "bs58", "chrono", "hex", @@ -3562,6 +3579,17 @@ dependencies = [ "keccak", ] +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3659,7 +3687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3672,6 +3700,12 @@ dependencies = [ "der", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "ssh-cipher" version = "0.3.0" @@ -3795,9 +3829,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -3837,10 +3871,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3880,7 +3914,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -3975,9 +4009,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -4003,9 +4037,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -4014,9 +4048,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -4162,7 +4196,7 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" dependencies = [ - "rand 0.10.2", + "rand 0.9.5", ] [[package]] @@ -4191,7 +4225,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4434,7 +4468,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4850,10 +4884,10 @@ version = "26.5.6" dependencies = [ "ansi-to-tui", "anyhow", - "base64", + "base64 0.23.0", "image", "inventory", - "moxcms", + "moxcms 0.9.0", "palette", "quantette", "ratatui-core", @@ -4956,7 +4990,7 @@ version = "26.5.6" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -5134,6 +5168,7 @@ dependencies = [ "anyhow", "arc-swap", "bitflags", + "cfg_aliases", "core-foundation-sys", "data-encoding", "dirs", @@ -5146,6 +5181,7 @@ dependencies = [ "mlua", "objc2", "parking_lot", + "percent-encoding", "rand 0.10.2", "regex", "scopeguard", @@ -5368,7 +5404,6 @@ version = "26.5.6" dependencies = [ "anyhow", "arc-swap", - "base64", "hashbrown 0.17.1", "mlua", "percent-encoding", @@ -5389,7 +5424,7 @@ name = "yazi-term" version = "26.5.6" dependencies = [ "anyhow", - "base64", + "base64 0.23.0", "bitflags", "compact_str 0.10.0", "futures", @@ -5411,7 +5446,7 @@ dependencies = [ name = "yazi-tty" version = "26.5.9" dependencies = [ - "base64", + "base64 0.23.0", "bitflags", "libc", "mlua", diff --git a/Cargo.toml b/Cargo.toml index af8f8145..e5b57182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,16 +38,16 @@ debug = false ansi-to-tui = "8.0.1" anyhow = "1.0.104" arc-swap = { version = "1.9.2", features = [ "serde" ] } -base64 = "0.22.1" +base64 = "0.23.0" bitflags = { version = "2.13.1", features = [ "serde" ] } chrono = "0.4.45" -clap = { version = "4.6.2", features = [ "derive" ] } +clap = { version = "4.6.4", features = [ "derive" ] } compact_str = { version = "0.10.0", features = [ "serde" ] } core-foundation-sys = "0.8.7" data-encoding = "2.11.0" dirs = "6.0.0" dyn-clone = "1.0.20" -either = { version = "1.16.0" } +either = { version = "1.17.0" } foldhash = "0.2.0" futures = "0.3.33" globset = "0.4.19" @@ -55,7 +55,7 @@ hashbrown = { version = "0.17.1", features = [ "serde" ] } image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] } indexmap = { version = "2.14.0", features = [ "serde" ] } inventory = "0.3.24" -libc = "0.2.186" +libc = "0.2.189" lru = "0.18.1" mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] } objc2 = "0.6.4" @@ -67,7 +67,7 @@ rand = { version = "0.10.2", default-features = false, features = ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] } ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] } regex = "1.13.1" -russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.62.4", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.229", features = [ "derive" ] } serde_json = "1.0.151" @@ -75,9 +75,9 @@ serde_with = "3.21.0" strum = { version = "0.28.0", features = [ "derive" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } thiserror = "2.0.19" -tokio = { version = "1.53.0", features = [ "full" ] } -tokio-stream = "0.1.18" -tokio-util = "0.7.18" +tokio = { version = "1.53.1", features = [ "full" ] } +tokio-stream = "0.1.19" +tokio-util = "0.7.19" toml = { version = "1.1.3" } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] } diff --git a/cspell.json b/cspell.json index 0685a4f8..4dce9d49 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"version":"0.2","language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable"]} \ No newline at end of file +{"language":"en","version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable","ents"]} \ No newline at end of file diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index 886c0128..96d1f699 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -28,7 +28,7 @@ anyhow = { workspace = true } base64 = { workspace = true } image = { workspace = true } inventory = { workspace = true } -moxcms = "0.8.1" +moxcms = "0.9.0" palette = { version = "0.7.6", default-features = false } quantette = { version = "0.6.0", default-features = false } ratatui-core = { workspace = true } diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 830012a2..1e65bbab 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -19,4 +19,4 @@ proc-macro = true # External dependencies proc-macro2 = "1" quote = "1.0.47" -syn = { version = "3.0.2", features = [ "full" ] } +syn = { version = "3.0.3", features = [ "full" ] } diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index b6d29768..2213c242 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -12,6 +12,9 @@ rust-version.workspace = true [lints] workspace = true +[build-dependencies] +cfg_aliases = "0.2.2" + [dependencies] yazi-binding = { path = "../yazi-binding", version = "26.5.6" } yazi-ffi = { path = "../yazi-ffi", version = "26.5.6" } @@ -45,8 +48,11 @@ typed-path = { workspace = true } [target."cfg(unix)".dependencies] uzers = { workspace = true } +[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))'.dependencies] +percent-encoding = { workspace = true } + [target.'cfg(windows)'.dependencies] -windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_UI_Shell_PropertiesSystem" ] } +windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_System_Variant", "Win32_UI_Shell_PropertiesSystem" ] } windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/yazi-fs/build.rs b/yazi-fs/build.rs new file mode 100644 index 00000000..2de9b63d --- /dev/null +++ b/yazi-fs/build.rs @@ -0,0 +1,13 @@ +fn main() { + cfg_aliases::cfg_aliases! { + trash_unsupported: { + any(target_os = "android", target_os = "ios") + }, + trash_unix: { + all(unix, not(trash_unsupported)) + }, + trash_freedesktop: { + all(unix, not(target_os = "macos"), not(trash_unsupported)) + }, + } +} diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index e5d0d897..2e1356a7 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -54,7 +54,7 @@ impl Cha { where T: AsStrand, { - Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta)) + Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta) | ChaKind::reparse(&meta)) } pub fn from_dummy(_url: U, r#type: Option) -> Self @@ -138,8 +138,8 @@ impl Cha { return self; } - let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM) | ChaKind::FOLLOW; - followed.unwrap_or(self).attach(retain) + let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM | ChaKind::REPARSE); + followed.unwrap_or(self).attach(retain | ChaKind::FOLLOW) } } @@ -165,6 +165,12 @@ impl Cha { #[inline] pub const fn is_dummy(self) -> bool { self.kind.contains(ChaKind::DUMMY) } + #[inline] + pub const fn is_reparse(self) -> bool { self.kind.contains(ChaKind::REPARSE) } + + #[inline] + pub fn is_indirect(self) -> bool { self.is_link() || self.is_reparse() } + pub fn atime_dur(self) -> anyhow::Result { if let Some(atime) = self.atime { Ok(atime.duration_since(UNIX_EPOCH)?) diff --git a/yazi-fs/src/cha/kind.rs b/yazi-fs/src/cha/kind.rs index 2bb4833c..0e0e1fa6 100644 --- a/yazi-fs/src/cha/kind.rs +++ b/yazi-fs/src/cha/kind.rs @@ -7,10 +7,11 @@ use yazi_shared::strand::AsStrand; bitflags! { #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct ChaKind: u8 { - const FOLLOW = 0b0000_0001; - const HIDDEN = 0b0000_0010; - const SYSTEM = 0b0000_0100; - const DUMMY = 0b0000_1000; + const FOLLOW = 0b0000_0001; + const HIDDEN = 0b0000_0010; + const SYSTEM = 0b0000_0100; + const DUMMY = 0b0000_1000; + const REPARSE = 0b0001_0000; } } @@ -43,4 +44,20 @@ impl ChaKind { me } + + #[inline] + pub(super) fn reparse(_meta: &Metadata) -> Self { + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + + use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; + + if _meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return Self::REPARSE; + } + } + + Self::empty() + } } diff --git a/yazi-fs/src/cha/lua.rs b/yazi-fs/src/cha/lua.rs index 6a1be252..991c37d4 100644 --- a/yazi-fs/src/cha/lua.rs +++ b/yazi-fs/src/cha/lua.rs @@ -48,6 +48,7 @@ impl UserData for Cha { fields.add_field_method_get("is_link", |_, me| Ok(me.is_link())); fields.add_field_method_get("is_orphan", |_, me| Ok(me.is_orphan())); fields.add_field_method_get("is_dummy", |_, me| Ok(me.is_dummy())); + fields.add_field_method_get("is_indirect", |_, me| Ok(me.is_indirect())); fields.add_field_method_get("is_block", |_, me| Ok(me.is_block())); fields.add_field_method_get("is_char", |_, me| Ok(me.is_char())); fields.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); diff --git a/yazi-fs/src/engine/local/calculator.rs b/yazi-fs/src/engine/local/calculator.rs index 5ad9afbb..3f51120a 100644 --- a/yazi-fs/src/engine/local/calculator.rs +++ b/yazi-fs/src/engine/local/calculator.rs @@ -17,7 +17,7 @@ impl SizeCalculator { let p = path.to_owned(); tokio::task::spawn_blocking(move || { let cha = Cha::new(p.file_name().unwrap_or_default(), std::fs::symlink_metadata(&p)?); - if !cha.is_dir() { + if !cha.is_dir() || cha.is_indirect() { return Ok(Self::Idle((VecDeque::new(), Some(cha.len)), cha)); } @@ -101,11 +101,27 @@ 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())); - } else if let Ok(meta) = ent.metadata() { - size += meta.len(); + + // If the entry is not a directory + if !ft.is_dir() { + size += ent.metadata().map_or(0, |meta| meta.len()); + continue; } + + // The entry is a directory, but it may be a reparse point + #[cfg(windows)] + { + let Ok(cha) = ent.metadata().map(|meta| Cha::new(ent.file_name(), meta)) else { + continue; + }; + if !cha.is_dir() || cha.is_indirect() { + size += cha.len; + continue; + } + } + + // Now, we can safely assume the entry is a regular directory + buf.push_back(Either::Left(ent.path())); } Some(size) } diff --git a/yazi-fs/src/engine/local/clear.rs b/yazi-fs/src/engine/local/clear.rs new file mode 100644 index 00000000..ddea57c3 --- /dev/null +++ b/yazi-fs/src/engine/local/clear.rs @@ -0,0 +1,126 @@ +use std::{io, path::Path}; + +#[cfg(unix)] +pub(super) fn remove_dir_clean_impl(path: &Path) -> io::Result<()> { + use std::{fs::OpenOptions, os::unix::fs::OpenOptionsExt}; + + use libc::{ELOOP, ENOENT, ENOTDIR, O_DIRECTORY, O_NOFOLLOW}; + + let dir = match OpenOptions::new().read(true).custom_flags(O_DIRECTORY | O_NOFOLLOW).open(path) { + Ok(dir) => dir, + Err(e) if e.raw_os_error().is_some_and(|e| matches!(e, ENOENT | ENOTDIR | ELOOP)) => { + return Ok(()); + } + Err(e) => return Err(e), + }; + + clear_no_follow(dir); + std::fs::remove_dir(path) +} + +#[cfg(unix)] +fn clear_no_follow(dir: std::fs::File) { + use std::{ffi::CStr, os::fd::{AsRawFd, FromRawFd, IntoRawFd}}; + + use libc::{AT_REMOVEDIR, DT_DIR, DT_UNKNOWN, O_CLOEXEC, O_DIRECTORY, O_NOFOLLOW, O_RDONLY}; + + struct Dropper(*mut libc::DIR); + + impl Drop for Dropper { + fn drop(&mut self) { _ = unsafe { libc::closedir(self.0) }; } + } + + let stream = unsafe { libc::fdopendir(dir.as_raw_fd()) }; + if stream.is_null() { + return; + } + + let _ = dir.into_raw_fd(); + let stream = Dropper(stream); + let fd = unsafe { libc::dirfd(stream.0) }; + + loop { + let entry = unsafe { libc::readdir(stream.0) }; + if entry.is_null() { + break; + } + + let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }; + if matches!(name.to_bytes(), b"." | b"..") { + continue; + } + + let ty = unsafe { (*entry).d_type }; + if ty != DT_DIR && ty != DT_UNKNOWN { + continue; + } + + let child = + unsafe { libc::openat(fd, name.as_ptr(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC) }; + if child < 0 { + continue; + } + + clear_no_follow(unsafe { std::fs::File::from_raw_fd(child) }); + _ = unsafe { libc::unlinkat(fd, name.as_ptr(), AT_REMOVEDIR) }; + } +} + +// --- Windows +#[cfg(windows)] +pub(super) fn remove_dir_clean_impl(path: &Path) -> io::Result<()> { + match clear_no_reparse(path) { + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + result => result, + } +} + +#[cfg(windows)] +fn clear_no_reparse(path: &Path) -> io::Result<()> { + use std::{fs::OpenOptions, os::windows::fs::{MetadataExt, OpenOptionsExt}}; + + use windows_sys::Win32::Storage::FileSystem::{DELETE, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE}; + + let dir = OpenOptions::new() + .access_mode(DELETE | FILE_READ_ATTRIBUTES) + .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) + .open(path)?; + + let attrs = dir.metadata()?.file_attributes(); + if attrs & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT) != FILE_ATTRIBUTE_DIRECTORY { + return Ok(()); + } + + if let Ok(entries) = std::fs::read_dir(path) { + for entry in entries.flatten() { + if entry.file_type().is_ok_and(|t| t.is_dir()) { + clear_no_reparse(&entry.path()).ok(); + } + } + } + + remove_dir(&dir) +} + +#[cfg(windows)] +fn remove_dir(dir: &std::fs::File) -> io::Result<()> { + use std::{mem, os::windows::io::AsRawHandle}; + + use windows_sys::Win32::{Foundation::HANDLE, Storage::FileSystem::{FILE_DISPOSITION_INFO, FileDispositionInfo, SetFileInformationByHandle}}; + + let info = FILE_DISPOSITION_INFO { DeleteFile: true }; + if unsafe { + SetFileInformationByHandle( + dir.as_raw_handle() as HANDLE, + FileDispositionInfo, + &info as *const FILE_DISPOSITION_INFO as _, + mem::size_of::() as u32, + ) + } == 0 + { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} diff --git a/yazi-fs/src/engine/local/local.rs b/yazi-fs/src/engine/local/local.rs index 7f1dcc63..e90ce21a 100644 --- a/yazi-fs/src/engine/local/local.rs +++ b/yazi-fs/src/engine/local/local.rs @@ -119,6 +119,12 @@ impl<'a> Engine for Local<'a> { #[inline] async fn remove_dir_all(&self) -> io::Result<()> { tokio::fs::remove_dir_all(self.path).await } + #[inline] + async fn remove_dir_clean(&self) -> io::Result<()> { + let path = self.path.to_owned(); + tokio::task::spawn_blocking(move || super::remove_dir_clean_impl(&path)).await? + } + #[inline] async fn remove_file(&self) -> io::Result<()> { tokio::fs::remove_file(self.path).await } diff --git a/yazi-fs/src/engine/local/mod.rs b/yazi-fs/src/engine/local/mod.rs index d837d4c3..95a973ef 100644 --- a/yazi-fs/src/engine/local/mod.rs +++ b/yazi-fs/src/engine/local/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry demand identical local read_dir); +yazi_macro::mod_flat!(absolute calculator casefold clear copier dir_entry demand identical local read_dir); diff --git a/yazi-fs/src/engine/traits.rs b/yazi-fs/src/engine/traits.rs index 4ee100fe..b6b6bd4f 100644 --- a/yazi-fs/src/engine/traits.rs +++ b/yazi-fs/src/engine/traits.rs @@ -128,9 +128,11 @@ pub trait Engine: Sized { { let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(())); while let Some(child) = it.next().await? { - let ft = ok_or_not_found!(child.file_type().await, continue); - let result = if ft.is_dir() { + let cha = ok_or_not_found!(child.metadata().await, continue); + let result = if cha.is_dir() && !cha.is_indirect() { Box::pin(remove_dir_all_impl::

(child.url().as_url())).await + } else if cha.is_dir() { + P::new(child.url().as_url()).await?.remove_dir().await } else { P::new(child.url().as_url()).await?.remove_file().await }; @@ -143,10 +145,12 @@ pub trait Engine: Sized { async move { let cha = ok_or_not_found!(self.symlink_metadata().await, return Ok(())); - if cha.is_link() { - self.remove_file().await - } else { + if !cha.is_indirect() { remove_dir_all_impl::(self.url()).await + } else if cha.is_dir() { + self.remove_dir().await + } else { + self.remove_file().await } } } @@ -159,18 +163,23 @@ pub trait Engine: Sized { let mut result = Ok(()); while let Some((dir, visited)) = stack.pop() { - let Ok(engine) = Self::new(dir.as_url()).await else { - continue; - }; + let Ok(engine) = Self::new(dir.as_url()).await else { continue }; if visited { result = engine.remove_dir().await; - } else if let Ok(mut it) = engine.read_dir().await { - stack.push((dir, true)); - while let Ok(Some(ent)) = it.next().await { - if ent.file_type().await.is_ok_and(|t| t.is_dir()) { - stack.push((ent.url(), false)); - } + continue; + } + + if !engine.symlink_metadata().await.is_ok_and(|c| c.is_dir() && !c.is_indirect()) { + continue; + } + + let Ok(mut it) = engine.read_dir().await else { continue }; + stack.push((dir, true)); + + while let Ok(Some(ent)) = it.next().await { + if ent.metadata().await.is_ok_and(|c| c.is_dir() && !c.is_indirect()) { + stack.push((ent.url(), false)); } } } diff --git a/yazi-fs/src/trash/common.rs b/yazi-fs/src/trash/common.rs new file mode 100644 index 00000000..c5e332c4 --- /dev/null +++ b/yazi-fs/src/trash/common.rs @@ -0,0 +1,18 @@ +#[cfg(trash_unix)] +pub(super) fn restore_item(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> { + use std::{fs, io}; + + let is_dir = fs::symlink_metadata(from)?.is_dir(); + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + + match if is_dir { fs::create_dir(to) } else { fs::File::create_new(to).map(|_| ()) } { + Ok(()) => fs::rename(from, to), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )), + Err(e) => Err(e), + } +} diff --git a/yazi-fs/src/trash/entries.rs b/yazi-fs/src/trash/entries.rs new file mode 100644 index 00000000..a945b96d --- /dev/null +++ b/yazi-fs/src/trash/entries.rs @@ -0,0 +1,38 @@ +use std::vec; + +use mlua::{FromLua, Lua, Value}; + +use super::{TrashEntry, TrashId}; + +pub(crate) struct TrashEntries(Vec); + +impl TrashEntries { + fn new(mut entries: Vec) -> Self { + entries.sort_unstable_by_key(|entry| entry.rel().components().count()); + + let mut seen = Vec::::with_capacity(entries.len()); + entries.retain(|entry| { + if seen.iter().any(|id| id.top() == entry.top() && entry.rel().starts_with(id.rel())) { + false + } else { + seen.push(entry.id.clone()); + true + } + }); + Self(entries) + } +} + +impl FromLua for TrashEntries { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let entries = Vec::::from_lua(value, lua)?; + Ok(Self::new(entries)) + } +} + +impl IntoIterator for TrashEntries { + type IntoIter = vec::IntoIter; + type Item = TrashEntry; + + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} diff --git a/yazi-fs/src/trash/entry.rs b/yazi-fs/src/trash/entry.rs index 717f6717..08107964 100644 --- a/yazi-fs/src/trash/entry.rs +++ b/yazi-fs/src/trash/entry.rs @@ -1,31 +1,80 @@ -use std::{ffi::OsString, path::PathBuf}; +use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}}; +#[cfg(trash_unix)] +use std::{ffi::OsString, io}; -use mlua::{IntoLua, Lua, Value}; +use mlua::{AnyUserData, FromLua, Lua, UserData, UserDataFields, Value}; use yazi_shared::path::PathBufDyn; +use yazi_shim::mlua::UserDataFieldsExt; +use super::TrashId; use crate::cha::Cha; #[derive(Clone, Debug)] -pub struct TrashEntry { - pub name: OsString, - pub key: OsString, - pub cha: Cha, - pub link_to: Option, - pub backing: PathBuf, +pub(crate) struct TrashEntry { + pub(super) id: TrashId, + pub(super) cha: Cha, + pub(super) lcha: Cha, + pub(super) original: Option, + pub(super) link_to: Option, + pub(super) backing: PathBuf, +} + +impl Deref for TrashEntry { + type Target = TrashId; + + fn deref(&self) -> &Self::Target { &self.id } } impl TrashEntry { - #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] - pub(super) fn new(path: PathBuf, name: OsString, key: OsString) -> std::io::Result { + #[cfg(trash_unix)] + pub(super) fn new(id: TrashId, backing: B, original: Option) -> io::Result + where + B: Into, + { use super::TrashCha; + let backing = backing.into(); - let cha = Cha::from_trash(&path, &name, true)?; - let link_to = if cha.is_link() { std::fs::read_link(&path).ok() } else { None }; + let name = id + .rel() + .file_name() + .or_else(|| original.as_deref().and_then(Path::file_name)) + .or_else(|| backing.file_name()) + .filter(|name| !name.is_empty()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))?; - Ok(Self { name, key, cha, link_to, backing: path }) + let (lcha, cha) = Cha::from_trash(&backing, name)?; + let link_to = if lcha.is_link() { std::fs::read_link(&backing).ok() } else { None }; + + Ok(Self { id, cha, lcha, original, link_to, backing }) } - #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + #[cfg(trash_unix)] + pub(super) fn top(top: T, backing: B, original: Option) -> io::Result + where + T: Into, + B: Into, + { + Self::new(TrashId::new(top, PathBuf::new())?, backing, original) + } + + #[cfg(trash_unix)] + pub(super) fn child(&self, name: OsString) -> io::Result { + let original = self.original.as_ref().map(|original| original.join(&name)); + Self::new(self.id.child(&name)?, self.backing.join(&name), original) + } + + pub(super) fn key(&self) -> &OsStr { self.rel().file_name().unwrap_or(self.top().as_os_str()) } + + pub(super) fn name(&self) -> &OsStr { + self + .rel() + .file_name() + .or_else(|| self.original.as_deref().and_then(Path::file_name)) + .or_else(|| self.backing.file_name()) + .expect("trash entry must have a name") + } + + #[cfg(trash_unix)] pub(super) fn into_file(self, url: impl Into) -> crate::file::File { use crate::file::{File, FileExtra}; @@ -37,17 +86,23 @@ impl TrashEntry { } } -impl IntoLua for TrashEntry { - fn into_lua(self, lua: &Lua) -> mlua::Result { - let Self { name, key, cha, link_to, backing } = self; - lua - .create_table_from([ - ("name", lua.create_external_string(name.into_encoded_bytes())?.into_lua(lua)?), - ("key", lua.create_external_string(key.into_encoded_bytes())?.into_lua(lua)?), - ("cha", cha.into_lua(lua)?), - ("link_to", link_to.map(PathBufDyn::Os).into_lua(lua)?), - ("backing", PathBufDyn::Os(backing).into_lua(lua)?), - ])? - .into_lua(lua) +impl FromLua for TrashEntry { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + AnyUserData::from_lua(value, lua)?.take() + } +} + +impl UserData for TrashEntry { + fn add_fields>(fields: &mut F) { + fields.add_cached_field("key", |lua, me| lua.create_string(me.key().as_encoded_bytes())); + fields.add_cached_field("top", |lua, me| { + lua.create_string(me.top().as_os_str().as_encoded_bytes()) + }); + fields.add_cached_field("rel", |_, me| Ok(PathBufDyn::from(me.rel()))); + fields.add_cached_field("name", |lua, me| lua.create_string(me.name().as_encoded_bytes())); + fields.add_cached_field("cha", |_, me| Ok(me.cha)); + fields.add_cached_field("original", |_, me| Ok(me.original.clone().map(PathBufDyn::Os))); + fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone().map(PathBufDyn::Os))); + fields.add_cached_field("backing", |_, me| Ok(PathBufDyn::Os(me.backing.clone()))); } } diff --git a/yazi-fs/src/trash/freedesktop.rs b/yazi-fs/src/trash/freedesktop.rs deleted file mode 100644 index ec6a2945..00000000 --- a/yazi-fs/src/trash/freedesktop.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::{ffi::OsStr, fs, hash::Hash, io, path::{Path, PathBuf}}; - -use hashbrown::HashMap; -use trash::{TrashItem, os_limited}; -use yazi_macro::ok_or_not_found; -use yazi_shim::Twox128; - -use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; -use crate::{cha::{Cha, ChaSig}, file::File}; - -pub struct Trash; - -impl Trash { - pub fn new() -> io::Result { Ok(Self) } - - pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { - let Some(node) = node else { - return os_limited::list() - .map_err(io::Error::other)? - .into_iter() - .map(|item| TrashEntry::new(Self::path(&item.id)?, item.name, item.id)) - .collect(); - }; - - fs::read_dir(self.resolve(node)?)? - .map(|entry| { - let entry = entry?; - let name = entry.file_name(); - let path = entry.path(); - TrashEntry::new(path, name.clone(), name) - }) - .collect() - } - - pub fn entry(&self, node: &TrashNode) -> io::Result { - let path = self.resolve(node)?; - let name = path - .file_name() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))? - .to_owned(); - TrashEntry::new(path, name, node.key.clone()) - } - - pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result { - let path = self.resolve(node)?; - if let Some(name) = node.rel.file_name() { - Cha::from_trash(&path, name, follow) - } else { - let item = self.top_item(&node.top)?; - Cha::from_trash(&path, &item.name, follow) - } - } - - pub(super) fn revalidate( - &self, - node: Option<&TrashNode>, - current: &File, - ) -> io::Result> { - let latest = if let Some(node) = node { - self.entry(node)?.into_file(¤t.url) - } else { - let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect(); - roots.sort_unstable(); - - let mut h = Twox128::default(); - for root in roots { - let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue); - let cha = Cha::new(root.file_name().unwrap_or_default(), meta); - - root.hash(&mut h); - ChaSig(cha).hash(&mut h); - } - - let hash = h.finish_128(); - File { - cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) }, - ..current.clone() - } - }; - - let changed = !latest.cha.hits(current.cha) - || latest.extra.link_to() != current.extra.link_to() - || latest.extra.backing() != current.extra.backing(); - - Ok(changed.then_some(latest)) - } - - pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { - if node.rel.as_os_str().is_empty() { - os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) - } else { - fs::remove_file(self.resolve(node)?) - } - } - - pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { - if node.rel.as_os_str().is_empty() { - os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) - } else { - fs::remove_dir(self.resolve(node)?) - } - } - - pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { - let mut tops = Vec::new(); - let items: HashMap<_, _> = os_limited::list() - .map_err(io::Error::other)? - .into_iter() - .map(|item| (item.id.clone(), item)) - .collect(); - - for node in nodes { - let item = items - .get(&node.top) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?; - if node.rel.as_os_str().is_empty() { - tops.push(item.clone()); - continue; - } - - let from = self.resolve(&node)?; - let to = item.original_path().join(node.rel); - - let is_dir = fs::symlink_metadata(&from)?.is_dir(); - if let Some(parent) = to.parent() { - fs::create_dir_all(parent)?; - } - - match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } { - Ok(()) => fs::rename(from, to), - Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( - io::ErrorKind::AlreadyExists, - format!("restore target already exists: {to:?}"), - )), - Err(e) => Err(e), - }?; - } - - os_limited::restore_all(tops).map_err(io::Error::other) - } - - pub fn empty(&self) -> io::Result<()> { - os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other) - } - - fn resolve(&self, node: &TrashNode) -> io::Result { - let path = Self::path(&node.top)?; - Ok(if node.rel.as_os_str().is_empty() { path } else { path.join(&node.rel) }) - } - - fn top_item(&self, key: &OsStr) -> io::Result { - os_limited::list() - .map_err(io::Error::other)? - .into_iter() - .find(|item| item.id == key) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists")) - } - - fn path(key: &OsStr) -> io::Result { - let info = Path::new(key); // Path of the trash info file, e.g. "~/.local/share/Trash/info/filename.txt.trashinfo" - let parent = info - .parent() - .and_then(|parent| parent.parent()) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?; - let stem = info - .file_stem() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?; - - Ok(parent.join("files").join(stem)) - } -} diff --git a/yazi-fs/src/trash/freedesktop/mod.rs b/yazi-fs/src/trash/freedesktop/mod.rs new file mode 100644 index 00000000..e69d95e4 --- /dev/null +++ b/yazi-fs/src/trash/freedesktop/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(trash trash_info); diff --git a/yazi-fs/src/trash/freedesktop/trash.rs b/yazi-fs/src/trash/freedesktop/trash.rs new file mode 100644 index 00000000..83c00376 --- /dev/null +++ b/yazi-fs/src/trash/freedesktop/trash.rs @@ -0,0 +1,150 @@ +use std::{fs::{self}, hash::Hash, io, path::Path}; + +use trash::os_limited; +use yazi_macro::ok_or_not_found; +use yazi_shim::Twox128; + +use super::{super::{TrashCha, TrashEntries, TrashEntry, TrashId, restore_item}, TrashInfo}; +use crate::{cha::{Cha, ChaSig}, file::File}; + +pub struct Trash; + +impl Trash { + pub(crate) fn new() -> io::Result { Ok(Self) } + + pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result> { + let Some(entry) = entry else { + return self.tops(); + }; + + // TODO + if !entry.lcha.is_dir() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory")); + } + + fs::read_dir(&entry.backing)? + .map(|dirent| { + let dirent = dirent?; + entry.child(dirent.file_name()) + }) + .collect() + } + + pub(crate) fn entry(&self, id: &TrashId) -> io::Result { + let info = TrashInfo::parse(id.top())?; + if !os_limited::trash_folders() + .map_err(io::Error::other)? + .iter() + .any(|folder| folder == &info.root) + { + return Err(io::Error::new(io::ErrorKind::NotFound, "trash item outside of trash folders")); + } + + if id.has_rel() && !fs::symlink_metadata(&info.backing)?.file_type().is_dir() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory")); + } + + let backing = info.backing.join(id.rel()); + TrashEntry::new(id.clone(), backing, Some(info.original.join(id.rel()))) + } + + pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result { + Ok(if follow { entry.cha } else { entry.lcha }) + } + + pub(crate) fn revalidate( + &self, + entry: Option<&TrashEntry>, + current: &File, + ) -> io::Result> { + let latest = if let Some(entry) = entry { + entry.clone().into_file(¤t.url) + } else { + let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect(); + roots.sort_unstable(); + + let mut h = Twox128::default(); + for root in roots { + let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue); + let cha = Cha::new(root.file_name().unwrap_or_default(), meta); + + root.hash(&mut h); + ChaSig(cha).hash(&mut h); + } + + let hash = h.finish_128(); + File { + cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) }, + ..current.clone() + } + }; + + let changed = !latest.cha.hits(current.cha) + || latest.extra.link_to() != current.extra.link_to() + || latest.extra.backing() != current.extra.backing(); + + Ok(changed.then_some(latest)) + } + + pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> { + fs::remove_file(&entry.backing)?; + if !entry.has_rel() { + fs::remove_file(entry.top())?; + } + Ok(()) + } + + pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> { + fs::remove_dir(&entry.backing)?; + if !entry.has_rel() { + fs::remove_file(entry.top())?; + } + Ok(()) + } + + pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> { + for entry in entries { + let to = entry.original.clone().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location") + })?; + + restore_item(&entry.backing, &to)?; + + if !entry.has_rel() { + fs::remove_file(entry.top())?; + } + } + Ok(()) + } + + // FIXME: also rename or remove the .trashinfo file in the info folder + pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> { + fs::rename(&entry.backing, path) + } + + pub(crate) fn empty(&self) -> io::Result<()> { + for entry in self.tops()? { + if entry.lcha.is_dir() { + fs::remove_dir_all(&entry.backing)?; + } else { + fs::remove_file(&entry.backing)?; + } + fs::remove_file(entry.top())?; + } + Ok(()) + } + + fn tops(&self) -> io::Result> { + let mut tops = Vec::new(); + for root in os_limited::trash_folders().map_err(io::Error::other)? { + for entry in ok_or_not_found!(fs::read_dir(root.join("info")), continue) { + let entry = entry?; + let info = entry.path(); + if let Ok(parsed) = TrashInfo::parse(&info) { + tops.push(TrashEntry::top(info, parsed.backing, Some(parsed.original))?); + } + } + } + Ok(tops) + } +} diff --git a/yazi-fs/src/trash/freedesktop/trash_info.rs b/yazi-fs/src/trash/freedesktop/trash_info.rs new file mode 100644 index 00000000..c0d5b488 --- /dev/null +++ b/yazi-fs/src/trash/freedesktop/trash_info.rs @@ -0,0 +1,96 @@ +use std::{borrow::Cow, ffi::OsStr, fs::File, io::{self, BufRead, BufReader}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}}; + +use percent_encoding::percent_decode; +use uzers::Users; +use yazi_shared::USERS_CACHE; +use yazi_shim::path::PathExt; + +pub(super) struct TrashInfo { + pub(super) root: PathBuf, + pub(super) backing: PathBuf, + pub(super) original: PathBuf, +} + +impl TrashInfo { + // Parses from a trashinfo path, e.g.: + // /home/alice/.local/share/Trash/info/cat.jpg.trashinfo + pub(super) fn parse(info: &Path) -> io::Result { + if info.extension() != Some(OsStr::new("trashinfo")) { + return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path")); + } + + // /home/alice/.local/share/Trash + let root = info + .parent() + .filter(|p| p.file_name() == Some(OsStr::new("info"))) + .and_then(Path::parent) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"))?; + + // cat.jpg + let stem = info + .file_stem() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"))?; + + let original = Self::parse_original(info, root)?; + if original.file_name().is_none() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid original trash path")); + } + + Ok(Self { root: root.to_owned(), backing: root.join("files").join(stem), original }) + } + + fn parse_original(info: &Path, root: &Path) -> io::Result { + let mut reader = BufReader::new(File::open(info)?); + let mut line = Vec::new(); + + reader.read_until(b'\n', &mut line)?; + Self::trim_line(&mut line); + if line != b"[Trash Info]" { + return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash info header")); + } + + loop { + line.clear(); + if reader.read_until(b'\n', &mut line)? == 0 { + return Err(io::Error::new(io::ErrorKind::InvalidData, "trash info has no Path")); + } + + Self::trim_line(&mut line); + let Some(value) = line.strip_prefix(b"Path=") else { continue }; + let decoded: Cow<[u8]> = percent_decode(value).into(); + + let path = Path::new(OsStr::from_bytes(decoded.as_ref())); + if path.as_os_str().is_empty() || !path.is_absolute() && path.has_parent_component() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid original trash path")); + } + + return Ok(if path.is_absolute() { + path.to_owned() + } else { + Self::mount_point(root)?.join(path) + }); + } + } + + // /mnt/disk/.Trash/1000 => /mnt/disk + // /home/alice/.local/share/Trash => /home/alice/.local/share + fn mount_point(root: &Path) -> io::Result<&Path> { + let uid = USERS_CACHE.get_current_uid().to_string(); + + if root.file_name() == Some(OsStr::new(&uid)) + && let Some(parent) = root.parent() + && parent.file_name() == Some(OsStr::new(".Trash")) + { + parent.parent() + } else { + root.parent() + } + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash mount point")) + } + + fn trim_line(line: &mut Vec) { + while matches!(line.last(), Some(b'\n' | b'\r')) { + line.pop(); + } + } +} diff --git a/yazi-fs/src/trash/lua.rs b/yazi-fs/src/trash/lua.rs index 36058e63..48d5b0d2 100644 --- a/yazi-fs/src/trash/lua.rs +++ b/yazi-fs/src/trash/lua.rs @@ -1,10 +1,11 @@ use std::io; -use mlua::{ExternalError, ExternalResult, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; +use mlua::{ExternalError, ExternalResult, FromLua, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; use tokio::task::spawn_blocking; use yazi_binding::Error; +use yazi_shared::path::{PathBufDyn, PathLike}; -use super::{Trash, TrashNode, TrashNodes}; +use super::{Trash, TrashEntries, TrashEntry, TrashId}; use crate::file::File; impl UserData for Trash { @@ -16,31 +17,72 @@ impl UserData for Trash { } }); - methods.add_async_function("list", |lua, node: Option| async move { - match spawn_blocking(move || Trash::new()?.list(node.as_ref())).await.into_lua_err()? { - Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua), - Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + methods.add_async_function("entry", |lua, value: Value| async move { + if let Value::UserData(ud) = value { + return ud.borrow::()?.clone().into_lua_multi(&lua); } - }); - methods.add_async_function("entry", |lua, node: TrashNode| async move { - match spawn_blocking(move || Trash::new()?.entry(&node)).await.into_lua_err()? { + let id = TrashId::from_lua(value, &lua)?; + match spawn_blocking(move || Trash::new()?.entry(&id)).await.into_lua_err()? { Ok(entry) => entry.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); - methods.add_async_function("metadata", |lua, (node, follow): (TrashNode, bool)| async move { - match spawn_blocking(move || Trash::new()?.metadata(&node, follow)).await.into_lua_err()? { + methods.add_async_function("list", |lua, entry: Option| async move { + match spawn_blocking(move || Trash::new()?.list(entry.as_ref())).await.into_lua_err()? { + Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("metadata", |lua, (entry, follow): (TrashEntry, bool)| async move { + match spawn_blocking(move || Trash::new()?.metadata(&entry, follow)).await.into_lua_err()? { Ok(cha) => cha.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); + methods.add_async_function( + "remove", + |lua, (kind, entry): (LuaString, TrashEntry)| async move { + let f: fn(&Trash, &TrashEntry) -> io::Result<()> = match &*kind.as_bytes() { + b"file" => Trash::remove_file, + b"dir" => Trash::remove_dir, + _ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?, + }; + + match spawn_blocking(move || f(&Trash::new()?, &entry)).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }, + ); + + methods.add_async_function( + "rename", + |lua, (entry, path): (TrashEntry, PathBufDyn)| async move { + match spawn_blocking(move || Trash::new()?.rename(&entry, path.as_os()?)) + .await + .into_lua_err()? + { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }, + ); + + methods.add_async_function("restore", |lua, entries: TrashEntries| async move { + match spawn_blocking(move || Trash::new()?.restore(entries)).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + methods.add_async_function( "revalidate", - |lua, (node, file): (Option, File)| async move { - match spawn_blocking(move || Trash::new()?.revalidate(node.as_ref(), &file)) + |lua, (entry, file): (Option, File)| async move { + match spawn_blocking(move || Trash::new()?.revalidate(entry.as_ref(), &file)) .await .into_lua_err()? { @@ -49,25 +91,5 @@ impl UserData for Trash { } }, ); - - methods.add_async_function("remove", |lua, (kind, node): (LuaString, TrashNode)| async move { - let f: fn(&Trash, &TrashNode) -> io::Result<()> = match &*kind.as_bytes() { - b"file" => Trash::remove_file, - b"dir" => Trash::remove_dir, - _ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?, - }; - - match spawn_blocking(move || f(&Trash::new()?, &node)).await.into_lua_err()? { - Ok(()) => true.into_lua_multi(&lua), - Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), - } - }); - - methods.add_async_function("restore", |lua, nodes: TrashNodes| async move { - match spawn_blocking(move || Trash::new()?.restore(nodes)).await.into_lua_err()? { - Ok(()) => true.into_lua_multi(&lua), - Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), - } - }); } } diff --git a/yazi-fs/src/trash/macos.rs b/yazi-fs/src/trash/macos.rs deleted file mode 100644 index b1a04cf5..00000000 --- a/yazi-fs/src/trash/macos.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}}; - -use ds_parser::Value; -use hashbrown::HashMap; -use yazi_macro::ok_or_not_found; - -use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; -use crate::{cha::Cha, file::File}; - -pub struct Trash; - -impl Trash { - pub fn new() -> io::Result { Ok(Self) } - - pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { - let it = match fs::read_dir(self.resolve(node)?) { - Ok(it) => it, - Err(e) if e.kind() == io::ErrorKind::NotFound && node.is_none() => return Ok(vec![]), - Err(e) => return Err(e), - }; - - it.map(|entry| { - let entry = entry?; - let name = entry.file_name(); - let path = entry.path(); - let key = if node.is_some() { name.clone() } else { path.as_os_str().to_owned() }; - TrashEntry::new(path, name, key) - }) - .collect() - } - - pub fn entry(&self, node: &TrashNode) -> io::Result { - let path = self.resolve(Some(node))?; - let name = path - .file_name() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))? - .to_owned(); - TrashEntry::new(path, name, node.key.clone()) - } - - pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result { - let path = self.resolve(Some(node))?; - Cha::from_trash(&path, path.file_name().unwrap_or_default(), follow) - } - - pub(super) fn revalidate( - &self, - node: Option<&TrashNode>, - current: &File, - ) -> io::Result> { - let latest = if let Some(node) = node { - self.entry(node)?.into_file(¤t.url) - } else { - let path = self.resolve(None)?; - let cha = match fs::symlink_metadata(&path) { - Ok(meta) => Cha::new(path.file_name().unwrap_or_default(), meta), - Err(e) if e.kind() == io::ErrorKind::NotFound => Cha::from_mold(true), - Err(e) => return Err(e), - }; - File { cha, ..current.clone() } - }; - - let changed = !latest.cha.hits(current.cha) - || latest.extra.link_to() != current.extra.link_to() - || latest.extra.backing() != current.extra.backing(); - - Ok(changed.then_some(latest)) - } - - pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { - fs::remove_file(self.resolve(Some(node))?) - } - - pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { - fs::remove_dir(self.resolve(Some(node))?) - } - - pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { - let root = self.resolve(None)?; - let locations = OriginalLocation::parse(&fs::read(root.join(".DS_Store"))?)?; - - for node in nodes { - let name = Path::new(&node.top).file_name().unwrap_or_default(); - let location = locations.get(name).ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - format!("original location is unavailable for trash item: {:?}", node.top), - ) - })?; - - let from = self.resolve(Some(&node))?; - let to = location.join(&node)?; - - let is_dir = fs::symlink_metadata(&from)?.is_dir(); - if let Some(parent) = to.parent() { - fs::create_dir_all(parent)?; - } - - match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } { - Ok(()) => fs::rename(from, to), - Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( - io::ErrorKind::AlreadyExists, - format!("restore target already exists: {to:?}"), - )), - Err(e) => Err(e), - }?; - } - - Ok(()) - } - - pub fn empty(&self) -> io::Result<()> { - let root = self.resolve(None)?; - for entry in ok_or_not_found!(fs::read_dir(root), return Ok(())) { - let entry = entry?; - if entry.file_type()?.is_dir() { - fs::remove_dir_all(entry.path())?; - } else { - fs::remove_file(entry.path())?; - } - } - Ok(()) - } - - fn resolve(&self, node: Option<&TrashNode>) -> io::Result { - if let Some(node) = node { - let top = Path::new(&node.top); - return Ok(if node.rel.as_os_str().is_empty() { top.into() } else { top.join(&node.rel) }); - } - - Ok( - dirs::home_dir() - .filter(|p| p.is_absolute()) - .ok_or_else(|| { - io::Error::other("cannot determine home directory for trash root resolution") - })? - .join(".Trash"), - ) - } -} - -// --- OriginalLocation -#[derive(Default)] -struct OriginalLocation { - parent: Option, - name: Option, -} - -impl OriginalLocation { - fn parse(bytes: &[u8]) -> io::Result> { - let store = ds_parser::parse(bytes).map_err(io::Error::other)?; - let mut locations = HashMap::::new(); - - for record in store.records { - let Value::Ustr(value) = record.value else { continue }; - if value.is_empty() { - continue; - } - - let location = locations.entry_ref(OsStr::new(&record.name)).or_default(); - match &record.field.fourcc().bytes() { - b"ptbL" => location.parent = Some(value.into()), - b"ptbN" => location.name = Some(value.into()), - _ => {} - } - } - - Ok(locations) - } - - fn join(&self, node: &TrashNode) -> io::Result { - let parent = self.parent.as_deref().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back location") - })?; - - let name = self.name.as_deref().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back name") - })?; - - let mut components = Path::new(name).components(); - if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() { - return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash put-back name")); - } - - let top_path = Path::new("/").join(parent).join(name); - Ok(if node.rel.as_os_str().is_empty() { top_path } else { top_path.join(&node.rel) }) - } -} diff --git a/yazi-fs/src/trash/macos/ds_store.rs b/yazi-fs/src/trash/macos/ds_store.rs new file mode 100644 index 00000000..fa1327ce --- /dev/null +++ b/yazi-fs/src/trash/macos/ds_store.rs @@ -0,0 +1,57 @@ +use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}}; + +use ds_parser::Value; +use hashbrown::HashMap; +use yazi_shim::path::PathExt; + +#[derive(Default)] +pub(super) struct DsStore { + parent: Option, + name: Option, +} + +impl DsStore { + pub(super) fn parse(path: &Path) -> io::Result> { + let bytes = fs::read(path)?; + let store = ds_parser::parse(&bytes).map_err(io::Error::other)?; + + let mut locations = HashMap::::new(); + for record in store.records { + let Value::Ustr(value) = record.value else { continue }; + if value.is_empty() { + continue; + } + + let location = locations.entry_ref(OsStr::new(&record.name)).or_default(); + match &record.field.fourcc().bytes() { + b"ptbL" => location.parent = Some(value.into()), + b"ptbN" => location.name = Some(value.into()), + _ => {} + } + } + + Ok(locations) + } + + pub(super) fn join(&self, rel: &Path) -> io::Result { + if !rel.is_relative() || rel.has_parent_component() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path")); + } + + let parent = self.parent.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back location") + })?; + + let name = self.name.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back name") + })?; + + let mut components = Path::new(name).components(); + if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash put-back name")); + } + + let top_path = Path::new("/").join(parent).join(name); + Ok(if rel.as_os_str().is_empty() { top_path } else { top_path.join(rel) }) + } +} diff --git a/yazi-fs/src/trash/macos/mod.rs b/yazi-fs/src/trash/macos/mod.rs new file mode 100644 index 00000000..b2294f43 --- /dev/null +++ b/yazi-fs/src/trash/macos/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(trash ds_store); diff --git a/yazi-fs/src/trash/macos/trash.rs b/yazi-fs/src/trash/macos/trash.rs new file mode 100644 index 00000000..74f0ec47 --- /dev/null +++ b/yazi-fs/src/trash/macos/trash.rs @@ -0,0 +1,133 @@ +use std::{fs, io, path::{Path, PathBuf}}; + +use yazi_macro::ok_or_not_found; + +use super::{super::{TrashCha, TrashEntries, TrashEntry, TrashId, restore_item}, DsStore}; +use crate::{cha::Cha, file::File}; + +pub struct Trash; + +impl Trash { + pub(crate) fn new() -> io::Result { Ok(Self) } + + pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result> { + if entry.is_some_and(|entry| !entry.lcha.is_dir()) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory")); + } + + let root = self.root()?; + let store = if entry.is_none() { + DsStore::parse(&root.join(".DS_Store")).unwrap_or_default() + } else { + Default::default() + }; + + let path = entry.map_or(&root, |entry| &entry.backing); + let it = match fs::read_dir(path) { + Ok(it) => it, + Err(e) if e.kind() == io::ErrorKind::NotFound && entry.is_none() => return Ok(vec![]), + Err(e) => return Err(e), + }; + + it.map(|dirent| { + let dirent = dirent?; + if let Some(entry) = entry { + entry.child(dirent.file_name()) + } else { + let path = dirent.path(); + let original = store + .get(path.file_name().unwrap_or_default()) + .and_then(|ds| ds.join(Path::new("")).ok()); + TrashEntry::top(path.clone(), path, original) + } + }) + .collect() + } + + pub(crate) fn entry(&self, id: &TrashId) -> io::Result { + let root = self.root()?; + if id.top().parent() != Some(&root) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "item is not in the trash")); + } + if id.has_rel() && !fs::symlink_metadata(id.top())?.file_type().is_dir() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory")); + } + + let store = DsStore::parse(&root.join(".DS_Store")).unwrap_or_default(); + let name = id.top().file_name().unwrap_or_default(); + let original = store.get(name).and_then(|ds| ds.join(id.rel()).ok()); + + TrashEntry::new(id.clone(), id.path(), original) + } + + pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result { + Ok(if follow { entry.cha } else { entry.lcha }) + } + + pub(crate) fn revalidate( + &self, + entry: Option<&TrashEntry>, + current: &File, + ) -> io::Result> { + let latest = if let Some(entry) = entry { + entry.clone().into_file(¤t.url) + } else { + let path = self.root()?; + let cha = match fs::symlink_metadata(&path) { + Ok(meta) => Cha::new(path.file_name().unwrap_or_default(), meta), + Err(e) if e.kind() == io::ErrorKind::NotFound => Cha::from_mold(true), + Err(e) => return Err(e), + }; + File { cha, ..current.clone() } + }; + + let changed = !latest.cha.hits(current.cha) + || latest.extra.link_to() != current.extra.link_to() + || latest.extra.backing() != current.extra.backing(); + + Ok(changed.then_some(latest)) + } + + pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> { + fs::remove_file(&entry.backing) + } + + pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> { + fs::remove_dir(&entry.backing) + } + + pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> { + for entry in entries { + let to = entry.original.as_ref().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location") + })?; + + restore_item(&entry.backing, &to)?; + } + Ok(()) + } + + pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> { + fs::rename(&entry.backing, path) + } + + pub(crate) fn empty(&self) -> io::Result<()> { + let root = self.root()?; + for dirent in ok_or_not_found!(fs::read_dir(root), return Ok(())) { + let dirent = dirent?; + if dirent.file_type()?.is_dir() { + fs::remove_dir_all(dirent.path())?; + } else { + fs::remove_file(dirent.path())?; + } + } + Ok(()) + } + + fn root(&self) -> io::Result { + dirs::home_dir() + .filter(|p| p.is_absolute()) + .ok_or_else(|| io::Error::other("cannot determine home directory for trash root resolution")) + .map(|home| home.join(".Trash")) + } +} diff --git a/yazi-fs/src/trash/mod.rs b/yazi-fs/src/trash/mod.rs index e48f2913..9b774b24 100644 --- a/yazi-fs/src/trash/mod.rs +++ b/yazi-fs/src/trash/mod.rs @@ -1,4 +1,7 @@ -yazi_macro::mod_flat!(entry lua node nodes); +yazi_macro::mod_flat!(entries entry lua trash_id); + +#[cfg(trash_unix)] +yazi_macro::mod_flat!(common); #[cfg(target_os = "macos")] yazi_macro::mod_flat!(macos); @@ -6,11 +9,11 @@ yazi_macro::mod_flat!(macos); #[cfg(windows)] yazi_macro::mod_flat!(windows); -#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))] +#[cfg(trash_freedesktop)] yazi_macro::mod_flat!(freedesktop); -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(trash_unsupported)] yazi_macro::mod_flat!(unsupported); -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(trash_unsupported))] yazi_macro::mod_flat!(traits); diff --git a/yazi-fs/src/trash/node.rs b/yazi-fs/src/trash/node.rs deleted file mode 100644 index dc97c26f..00000000 --- a/yazi-fs/src/trash/node.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::{ffi::OsString, io, path::{Component, PathBuf}}; - -use mlua::{BorrowedBytes, FromLua, IntoLua, Lua, Table, Value}; -use yazi_shared::{path::PathBufDyn, strand::AsStrand}; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TrashNode { - pub(super) key: OsString, - pub(super) top: OsString, - pub(super) rel: PathBuf, -} - -impl TrashNode { - pub(super) fn validate(&self) -> io::Result<()> { - if self.key.is_empty() || self.top.is_empty() || !self.rel.is_relative() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node")); - } - if self.rel.components().any(|c| matches!(c, Component::ParentDir)) { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node path")); - } - Ok(()) - } -} - -impl FromLua for TrashNode { - fn from_lua(value: Value, lua: &Lua) -> mlua::Result { - let t = Table::from_lua(value, lua)?; - let node = Self { - key: t.raw_get::("key")?.as_strand().to_os_string()?, - top: t.raw_get::("top")?.as_strand().to_os_string()?, - rel: t.raw_get::("rel")?.into_os()?, - }; - - node.validate().map_err(mlua::Error::external)?; - Ok(node) - } -} - -impl IntoLua for TrashNode { - fn into_lua(self, lua: &Lua) -> mlua::Result { - lua - .create_table_from([ - ("key", lua.create_external_string(self.key.into_encoded_bytes())?.into_lua(lua)?), - ("top", lua.create_external_string(self.top.into_encoded_bytes())?.into_lua(lua)?), - ("rel", PathBufDyn::from(self.rel).into_lua(lua)?), - ])? - .into_lua(lua) - } -} diff --git a/yazi-fs/src/trash/nodes.rs b/yazi-fs/src/trash/nodes.rs deleted file mode 100644 index d6521780..00000000 --- a/yazi-fs/src/trash/nodes.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::vec; - -use mlua::{FromLua, Lua, Value}; - -use super::TrashNode; - -pub struct TrashNodes(Vec); - -impl TrashNodes { - fn new(mut nodes: Vec) -> Self { - nodes.sort_unstable_by_key(|node| node.rel.components().count()); - - let mut seen = Vec::::with_capacity(nodes.len()); - for node in nodes { - if seen.iter().any(|parent| parent.top == node.top && node.rel.starts_with(&parent.rel)) { - continue; - } - seen.push(node); - } - Self(seen) - } -} - -impl FromLua for TrashNodes { - fn from_lua(value: Value, lua: &Lua) -> mlua::Result { - let nodes = Vec::::from_lua(value, lua)?; - Ok(Self::new(nodes)) - } -} - -impl IntoIterator for TrashNodes { - type IntoIter = vec::IntoIter; - type Item = TrashNode; - - fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } -} diff --git a/yazi-fs/src/trash/traits.rs b/yazi-fs/src/trash/traits.rs index 2c84c766..5c046d0f 100644 --- a/yazi-fs/src/trash/traits.rs +++ b/yazi-fs/src/trash/traits.rs @@ -1,14 +1,11 @@ +use std::fs; + use crate::cha::{Cha, ChaKind, ChaMode}; pub(super) trait TrashCha: Sized { fn from_mold(is_dir: bool) -> Self; - #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] - fn from_trash( - path: &std::path::Path, - name: &std::ffi::OsStr, - follow: bool, - ) -> std::io::Result; + fn from_trash(path: &std::path::Path, name: &std::ffi::OsStr) -> std::io::Result<(Self, Self)>; } impl TrashCha for Cha { @@ -20,17 +17,13 @@ impl TrashCha for Cha { cha } - #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] - fn from_trash( - path: &std::path::Path, - name: &std::ffi::OsStr, - follow: bool, - ) -> std::io::Result { - let cha = Cha::new(name, std::fs::symlink_metadata(path)?); - Ok(if cha.is_link() && follow { - cha.follow(std::fs::metadata(path).ok().map(|meta| Cha::new(name, meta))) + fn from_trash(path: &std::path::Path, name: &std::ffi::OsStr) -> std::io::Result<(Self, Self)> { + let lcha = Cha::new(name, fs::symlink_metadata(path)?); + let cha = if lcha.is_link() { + lcha.follow(fs::metadata(path).ok().map(|meta| Cha::new(name, meta))) } else { - cha - }) + lcha + }; + Ok((lcha, cha)) } } diff --git a/yazi-fs/src/trash/trash_id.rs b/yazi-fs/src/trash/trash_id.rs new file mode 100644 index 00000000..9eb9ea2f --- /dev/null +++ b/yazi-fs/src/trash/trash_id.rs @@ -0,0 +1,59 @@ +use std::{ffi::OsStr, io, path::{Path, PathBuf}}; + +use mlua::{BorrowedBytes, ExternalResult, FromLua, Lua, Table, Value}; +use yazi_shared::{path::PathBufDyn, strand::AsStrand}; +use yazi_shim::path::PathExt; + +#[derive(Clone, Debug)] +pub(crate) struct TrashId { + top: PathBuf, + rel: PathBuf, +} + +impl TrashId { + pub(super) fn new(top: T, rel: R) -> io::Result + where + T: Into, + R: Into, + { + let top = top.into(); + let rel = rel.into(); + + if top.as_os_str().is_empty() || !rel.is_relative() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry")); + } + if rel.has_parent_component() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path")); + } + + Ok(Self { top, rel }) + } + + pub(super) fn top(&self) -> &Path { &self.top } + + pub(super) fn rel(&self) -> &Path { &self.rel } + + #[cfg(target_os = "macos")] + pub(super) fn path(&self) -> PathBuf { self.top.join(&self.rel) } + + pub(super) fn child(&self, name: &OsStr) -> io::Result { + let rel = self.rel.join(name); + if !rel.is_relative() || rel.has_parent_component() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path")); + } + + Ok(Self { top: self.top.clone(), rel }) + } + + pub(super) fn has_rel(&self) -> bool { !self.rel.as_os_str().is_empty() } +} + +impl FromLua for TrashId { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let t = Table::from_lua(value, lua)?; + + let top = t.raw_get::("top")?.as_strand().to_os_path()?; + let rel = t.raw_get::("rel")?.into_os()?; + Self::new(top, rel).into_lua_err() + } +} diff --git a/yazi-fs/src/trash/unsupported/mod.rs b/yazi-fs/src/trash/unsupported/mod.rs new file mode 100644 index 00000000..eec35b1b --- /dev/null +++ b/yazi-fs/src/trash/unsupported/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(trash); diff --git a/yazi-fs/src/trash/unsupported.rs b/yazi-fs/src/trash/unsupported/trash.rs similarity index 57% rename from yazi-fs/src/trash/unsupported.rs rename to yazi-fs/src/trash/unsupported/trash.rs index 464e4026..038ebb59 100644 --- a/yazi-fs/src/trash/unsupported.rs +++ b/yazi-fs/src/trash/unsupported/trash.rs @@ -1,6 +1,6 @@ -use std::io; +use std::{io, path::Path}; -use super::{TrashEntry, TrashNode, TrashNodes}; +use super::super::{TrashEntries, TrashEntry, TrashId}; use crate::{cha::Cha, file::File}; pub struct Trash; @@ -8,35 +8,39 @@ pub struct Trash; impl Trash { pub fn new() -> io::Result { Ok(Self) } - pub fn list(&self, _node: Option<&TrashNode>) -> io::Result> { + pub fn list(&self, _entry: Option<&TrashEntry>) -> io::Result> { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } - pub fn entry(&self, _node: &TrashNode) -> io::Result { + pub fn entry(&self, _id: &TrashId) -> io::Result { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } - pub fn metadata(&self, _node: &TrashNode, _: bool) -> io::Result { + pub fn metadata(&self, _entry: &TrashEntry, _: bool) -> io::Result { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } - pub(super) fn revalidate( + pub(crate) fn revalidate( &self, - _node: Option<&TrashNode>, + _entry: Option<&TrashEntry>, _current: &File, ) -> io::Result> { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } - pub fn remove_file(&self, _node: &TrashNode) -> io::Result<()> { + pub fn remove_file(&self, _entry: &TrashEntry) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } - pub fn remove_dir(&self, _node: &TrashNode) -> io::Result<()> { + pub fn remove_dir(&self, _entry: &TrashEntry) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } - pub fn restore(&self, _nodes: TrashNodes) -> io::Result<()> { + pub fn restore(&self, _entries: TrashEntries) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn rename(&self, _entry: &TrashEntry, _path: &Path) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) } diff --git a/yazi-fs/src/trash/windows.rs b/yazi-fs/src/trash/windows.rs deleted file mode 100644 index 167f068d..00000000 --- a/yazi-fs/src/trash/windows.rs +++ /dev/null @@ -1,330 +0,0 @@ -use std::{ffi::{OsStr, OsString, c_void}, fs, hash::Hash, io, mem, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}}; - -use hashbrown::HashMap; -use trash::{TrashItem, os_limited}; -use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::*, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}}; -use yazi_ffi::Com; -use yazi_shim::Twox128; - -use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; -use crate::{cha::Cha, file::File}; - -thread_local! { - static COM: io::Result = Com::new(); -} - -pub struct Trash; - -impl Trash { - pub fn new() -> io::Result { - COM.with(|result| { - result.as_ref().map(|_| Self).map_err(|e| io::Error::new(e.kind(), e.to_string())) - }) - } - - pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { - let Some(node) = node else { - return os_limited::list() - .map_err(io::Error::other)? - .into_iter() - .map(|item| ShellItem::new(&item.id)?.entry(item.name, item.id)) - .collect(); - }; - - self - .resolve(node)? - .children()? - .into_iter() - .map(|item| { - let name = item.display_name(SIGDN_PARENTRELATIVE)?; - item.entry(name.clone(), name) - }) - .collect() - } - - pub fn entry(&self, node: &TrashNode) -> io::Result { - let item = self.resolve(node)?; - item.entry(item.display_name(SIGDN_PARENTRELATIVE)?, &node.key) - } - - pub fn metadata(&self, node: &TrashNode, _: bool) -> io::Result { self.resolve(node)?.cha() } - - pub(super) fn revalidate( - &self, - node: Option<&TrashNode>, - current: &File, - ) -> io::Result> { - let cha = - if let Some(node) = node { TrashSig::item(&self.resolve(node)?)? } else { TrashSig::root()? }; - - Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) }) - } - - pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { - if node.rel.as_os_str().is_empty() { - os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) - } else { - self.resolve(node)?.delete() - } - } - - pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { - if node.rel.as_os_str().is_empty() { - return os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other); - } - - let item = self.resolve(node)?; - if !item.children()?.is_empty() { - return Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty")); - } - item.delete() - } - - pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { - let mut tops = Vec::new(); - let items: HashMap<_, _> = os_limited::list() - .map_err(io::Error::other)? - .into_iter() - .map(|item| (item.id.clone(), item)) - .collect(); - - for node in nodes { - let item = items - .get(&node.top) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?; - if node.rel.as_os_str().is_empty() { - tops.push(item.clone()); - } else { - let to = item.original_path().join(&node.rel); - self.restore_do(&self.resolve(&node)?, &to)?; - } - } - os_limited::restore_all(tops).map_err(io::Error::other) - } - - fn restore_do(&self, item: &ShellItem, to: &Path) -> io::Result<()> { - match fs::symlink_metadata(to) { - Ok(_) => { - return Err(io::Error::new( - io::ErrorKind::AlreadyExists, - format!("restore target already exists: {to:?}"), - )); - } - Err(e) if e.kind() == io::ErrorKind::NotFound => {} - Err(e) => return Err(e), - } - - // Create parent directories - let parent = to - .parent() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?; - fs::create_dir_all(parent)?; - - let parent = ShellItem::new(parent.as_os_str())?; - let name: Vec = to - .file_name() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))? - .encode_wide() - .chain([0]) - .collect(); - - operate(FOF_NO_UI | FOFX_EARLYFAILURE, |operation| unsafe { - operation.MoveItem(&item.0, &parent.0, PCWSTR(name.as_ptr()), None) - }) - } - - pub fn empty(&self) -> io::Result<()> { - os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other) - } - - fn resolve(&self, node: &TrashNode) -> io::Result { - let top = ShellItem::new(&node.top)?; - if node.rel.as_os_str().is_empty() { - return Ok(top); - } - - let path = PathBuf::from(top.display_name(SIGDN_FILESYSPATH)?).join(&node.rel); - ShellItem::new(path.as_os_str()) - } - - fn top_item(&self, key: &OsStr) -> io::Result { - os_limited::list() - .map_err(io::Error::other)? - .into_iter() - .find(|item| item.id == key) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists")) - } -} - -// --- TrashSig -struct TrashSig { - names: Option>, - count_size: Option<(i64, i64)>, -} - -impl TrashSig { - fn root() -> io::Result { - let root = ShellItem::root()?; - let cha = root.cha()?; - - let mut info = - SHQUERYRBINFO { cbSize: mem::size_of::() as u32, ..Default::default() }; - - let sig = if unsafe { SHQueryRecycleBinW(PCWSTR::null(), &mut info) }.is_ok() { - Self { names: None, count_size: Some((info.i64NumItems, info.i64Size)) } - } else if cha.mtime.is_some() { - Self { names: None, count_size: None } - } else { - Self { names: Some(Self::names(&root)?), count_size: None } - }; - - Ok(sig.into_cha(cha)) - } - - fn item(item: &ShellItem) -> io::Result { - let cha = item.cha()?; - let sig = if cha.mtime.is_none() && cha.is_dir() { - Self { names: Some(Self::names(item)?), count_size: None } - } else { - Self { names: None, count_size: None } - }; - - Ok(sig.into_cha(cha)) - } - - fn names(item: &ShellItem) -> io::Result> { - let mut names: Vec<_> = item - .children()? - .into_iter() - .map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)) - .collect::>()?; - names.sort_unstable(); - Ok(names) - } - - fn into_cha(self, mut cha: Cha) -> Cha { - let mut h = Twox128::default(); - if let Some((count, size)) = self.count_size { - (size, count).hash(&mut h); - } else if let Some(names) = self.names { - names.hash(&mut h); - } else { - return cha; - } - - let hash = h.finish_128(); - cha.ctime = UNIX_EPOCH.checked_add(Duration::from_nanos(hash as u64 ^ (hash >> 64) as u64)); - cha - } -} - -// --- ShellItem -struct ShellItem(IShellItem); - -impl ShellItem { - fn new(name: &OsStr) -> io::Result { - let name: Vec = name.encode_wide().chain([0]).collect(); - unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error) - } - - fn root() -> io::Result { - unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) } - .map(Self) - .map_err(error) - } - - fn children(&self) -> io::Result> { - let items: IEnumShellItems = - unsafe { self.0.BindToHandler(None, &BHID_EnumItems).map_err(error)? }; - - let mut result = Vec::new(); - loop { - let mut fetched = 0; - let mut next = [None]; - unsafe { items.Next(&mut next, Some(&mut fetched)).map_err(error)? }; - if fetched == 0 { - break; - } else if let Some(item) = next[0].take() { - result.push(Self(item)); - } - } - Ok(result) - } - - fn cha(&self) -> io::Result { - if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) { - match fs::metadata(&backing) { - Ok(meta) => return Ok(Cha::new(backing.file_name().unwrap_or_default(), meta)), - Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(e), - Err(_) => {} - } - } - - let item: IShellItem2 = self.0.cast().map_err(error)?; - let is_dir = unsafe { self.0.GetAttributes(SFGAO_FOLDER) }.map_err(error)? == SFGAO_FOLDER; - - let mut cha = Cha::from_mold(is_dir); - cha.len = unsafe { item.GetUInt64(&PKEY_Size) }.unwrap_or_default(); - cha.mtime = unsafe { item.GetFileTime(&PKEY_DateModified) }.ok().and_then(system_time); - Ok(cha) - } - - fn entry(&self, name: N, key: K) -> io::Result - where - N: Into, - K: Into, - { - Ok(TrashEntry { - name: name.into(), - key: key.into(), - cha: self.cha()?, - link_to: None, - backing: self.display_name(SIGDN_FILESYSPATH)?.into(), - }) - } - - fn delete(&self) -> io::Result<()> { - operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) }) - } - - fn display_name(&self, kind: SIGDN) -> io::Result { - unsafe { - let name = self.0.GetDisplayName(kind).map_err(error)?; - let result = OsString::from_wide(name.as_wide()); - CoTaskMemFree(Some(name.0.cast::())); - Ok(result) - } - } -} - -fn operate(flags: FILEOPERATION_FLAGS, f: F) -> io::Result<()> -where - F: FnOnce(&IFileOperation) -> windows::core::Result<()>, -{ - let aborted = unsafe { - let operation: IFileOperation = - CoCreateInstance(&FileOperation, None, CLSCTX_ALL).map_err(error)?; - operation.SetOperationFlags(flags).map_err(error)?; - f(&operation).map_err(error)?; - operation.PerformOperations().map_err(error)?; - operation.GetAnyOperationsAborted().map_err(error)?.as_bool() - }; - - if aborted { Err(io::Error::other("trash operation was aborted")) } else { Ok(()) } -} - -fn error(error: windows::core::Error) -> io::Error { - if error.code() == ERROR_FILE_NOT_FOUND.to_hresult() - || error.code() == ERROR_PATH_NOT_FOUND.to_hresult() - { - io::Error::new(io::ErrorKind::NotFound, error) - } else { - io::Error::other(error) - } -} - -fn system_time(time: FILETIME) -> Option { - let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime); - let ticks = ticks.checked_sub(116_444_736_000_000_000)?; - Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100)) -} diff --git a/yazi-fs/src/trash/windows/mod.rs b/yazi-fs/src/trash/windows/mod.rs new file mode 100644 index 00000000..3cfc874b --- /dev/null +++ b/yazi-fs/src/trash/windows/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(shell_item trash trash_sig); diff --git a/yazi-fs/src/trash/windows/shell_item.rs b/yazi-fs/src/trash/windows/shell_item.rs new file mode 100644 index 00000000..f87f5c6b --- /dev/null +++ b/yazi-fs/src/trash/windows/shell_item.rs @@ -0,0 +1,96 @@ +use std::{ffi::{OsStr, OsString, c_void}, io, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}}; + +use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::{StructuredStorage::PropVariantToBSTR, *}, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}}; + +use super::{super::{TrashCha, TrashEntry, TrashId}, trash::{error, operate, system_time}}; +use crate::cha::Cha; + +pub(super) struct ShellItem(pub(super) IShellItem); + +impl ShellItem { + pub(super) fn new(name: impl AsRef) -> io::Result { + let name: Vec = name.as_ref().encode_wide().chain([0]).collect(); + unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error) + } + + pub(super) fn root() -> io::Result { + unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) } + .map(Self) + .map_err(error) + } + + pub(super) fn top(name: &Path) -> io::Result { + Self::root()? + .children()? + .into_iter() + .find(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING).is_ok_and(|s| s == name)) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "item is not in the recycle bin")) + } + + pub(super) fn children(&self) -> io::Result> { + let items: IEnumShellItems = + unsafe { self.0.BindToHandler(None, &BHID_EnumItems).map_err(error)? }; + + let mut result = Vec::new(); + loop { + let mut fetched = 0; + let mut next = [None]; + unsafe { items.Next(&mut next, Some(&mut fetched)).map_err(error)? }; + if fetched == 0 { + break; + } else if let Some(item) = next[0].take() { + result.push(Self(item)); + } + } + Ok(result) + } + + pub(super) fn cha(&self) -> io::Result { + if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) { + let name = backing.file_name().unwrap_or_default(); + match Cha::from_trash(&backing, name) { + Ok((_, cha)) => return Ok(cha), + Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(e), + Err(_) => {} + } + } + + let item: IShellItem2 = self.0.cast().map_err(error)?; + let is_dir = unsafe { self.0.GetAttributes(SFGAO_FOLDER) }.map_err(error)? == SFGAO_FOLDER; + + let mut cha = Cha::from_mold(is_dir); + cha.len = unsafe { item.GetUInt64(&PKEY_Size) }.unwrap_or_default(); + cha.mtime = unsafe { item.GetFileTime(&PKEY_DateModified) }.ok().and_then(system_time); + Ok(cha) + } + + pub(super) fn original(&self) -> io::Result { + let item: IShellItem2 = self.0.cast().map_err(error)?; + let value = unsafe { + item.GetProperty(&PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_FROM }) + } + .map_err(error)?; + + let parent = unsafe { PropVariantToBSTR(&value) }.map_err(error)?; + Ok(PathBuf::from(OsString::from_wide(&parent)).join(self.display_name(SIGDN_PARENTRELATIVE)?)) + } + + pub(super) fn entry(&self, id: TrashId, original: Option) -> io::Result { + let backing: PathBuf = self.display_name(SIGDN_FILESYSPATH)?.into(); + let (lcha, cha) = Cha::from_trash(&backing, backing.file_name().unwrap_or_default())?; + Ok(TrashEntry { id, cha, lcha, original, link_to: None, backing }) + } + + pub(super) fn delete(&self) -> io::Result<()> { + operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) }) + } + + pub(super) fn display_name(&self, kind: SIGDN) -> io::Result { + unsafe { + let name = self.0.GetDisplayName(kind).map_err(error)?; + let result = OsString::from_wide(name.as_wide()); + CoTaskMemFree(Some(name.0.cast::())); + Ok(result) + } + } +} diff --git a/yazi-fs/src/trash/windows/trash.rs b/yazi-fs/src/trash/windows/trash.rs new file mode 100644 index 00000000..adf4a498 --- /dev/null +++ b/yazi-fs/src/trash/windows/trash.rs @@ -0,0 +1,199 @@ +use std::{fs, io, os::windows::ffi::OsStrExt, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}}; + +use windows::{Win32::{Foundation::*, System::Com::*, UI::Shell::*}, core::PCWSTR}; +use yazi_ffi::Com; + +use super::{super::{TrashEntries, TrashEntry, TrashId}, shell_item::ShellItem, trash_sig::TrashSig}; +use crate::{cha::Cha, file::File}; + +thread_local! { + static COM: io::Result = Com::new(); +} + +pub struct Trash; + +impl Trash { + pub(crate) fn new() -> io::Result { + COM.with(|result| { + result.as_ref().map(|_| Self).map_err(|e| io::Error::new(e.kind(), e.to_string())) + }) + } + + pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result> { + let Some(entry) = entry else { + return self.tops(); + }; + + if !entry.lcha.is_dir() || entry.lcha.is_indirect() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory")); + } + + let original = entry.original.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location") + })?; + + self + .resolve(entry)? + .children()? + .into_iter() + .map(|item| { + let name = item.display_name(SIGDN_PARENTRELATIVE)?; + item.entry(entry.id.child(&name)?, Some(original.join(&name))) + }) + .collect() + } + + pub(crate) fn entry(&self, id: &TrashId) -> io::Result { + let top = ShellItem::top(id.top())?; + let original = top.original()?.join(id.rel()); + if !id.has_rel() { + return top.entry(id.clone(), Some(original)); + } + + let backing: PathBuf = top.display_name(SIGDN_FILESYSPATH)?.into(); + let cha = Cha::new(backing.file_name().unwrap_or_default(), fs::symlink_metadata(&backing)?); + if cha.is_dir() && !cha.is_indirect() { + ShellItem::new(backing.join(id.rel()))?.entry(id.clone(), Some(original)) + } else { + Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory")) + } + } + + pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result { + Ok(if follow { entry.cha } else { entry.lcha }) + } + + pub(crate) fn revalidate( + &self, + entry: Option<&TrashEntry>, + current: &File, + ) -> io::Result> { + let cha = if let Some(entry) = entry { + TrashSig::item(&self.resolve(entry)?)? + } else { + TrashSig::root()? + }; + + Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) }) + } + + pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> { + self.resolve(entry)?.delete() + } + + pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> { + let item = self.resolve(entry)?; + if !entry.has_rel() { + item.delete() + } else if !item.children()?.is_empty() { + Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty")) + } else { + item.delete() + } + } + + pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> { + for entry in entries { + let to = entry.original.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location") + })?; + + self.restore_do(&self.resolve(&entry)?, &to)?; + } + Ok(()) + } + + pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> { + fs::rename(self.resolve(entry)?.display_name(SIGDN_FILESYSPATH)?, path) + } + + fn restore_do(&self, item: &ShellItem, to: &Path) -> io::Result<()> { + match fs::symlink_metadata(to) { + Ok(_) => { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )); + } + Err(e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + + // Create parent directories + let parent = to + .parent() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?; + fs::create_dir_all(parent)?; + + let parent = ShellItem::new(parent.as_os_str())?; + let name: Vec = to + .file_name() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))? + .encode_wide() + .chain([0]) + .collect(); + + operate(FOF_NO_UI | FOFX_EARLYFAILURE, |operation| unsafe { + operation.MoveItem(&item.0, &parent.0, PCWSTR(name.as_ptr()), None) + }) + } + + pub(crate) fn empty(&self) -> io::Result<()> { + unsafe { + SHEmptyRecycleBinW( + None, + PCWSTR::null(), + SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND, + ) + } + .map_err(error) + } + + fn resolve(&self, entry: &TrashEntry) -> io::Result { + if entry.has_rel() { ShellItem::new(&entry.backing) } else { ShellItem::top(entry.top()) } + } + + fn tops(&self) -> io::Result> { + ShellItem::root()? + .children()? + .into_iter() + .map(|item| { + let top = item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)?; + let original = item.original()?; + item.entry(TrashId::new(top, PathBuf::new())?, Some(original)) + }) + .collect() + } +} + +pub(super) fn operate(flags: FILEOPERATION_FLAGS, f: F) -> io::Result<()> +where + F: FnOnce(&IFileOperation) -> windows::core::Result<()>, +{ + let aborted = unsafe { + let operation: IFileOperation = + CoCreateInstance(&FileOperation, None, CLSCTX_ALL).map_err(error)?; + operation.SetOperationFlags(flags).map_err(error)?; + f(&operation).map_err(error)?; + operation.PerformOperations().map_err(error)?; + operation.GetAnyOperationsAborted().map_err(error)?.as_bool() + }; + + if aborted { Err(io::Error::other("trash operation was aborted")) } else { Ok(()) } +} + +pub(super) fn error(error: windows::core::Error) -> io::Error { + if error.code() == ERROR_FILE_NOT_FOUND.to_hresult() + || error.code() == ERROR_PATH_NOT_FOUND.to_hresult() + { + io::Error::new(io::ErrorKind::NotFound, error) + } else { + io::Error::other(error) + } +} + +pub(super) fn system_time(time: FILETIME) -> Option { + let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime); + let ticks = ticks.checked_sub(116_444_736_000_000_000)?; + Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100)) +} diff --git a/yazi-fs/src/trash/windows/trash_sig.rs b/yazi-fs/src/trash/windows/trash_sig.rs new file mode 100644 index 00000000..e3ebddd0 --- /dev/null +++ b/yazi-fs/src/trash/windows/trash_sig.rs @@ -0,0 +1,68 @@ +use std::{ffi::OsString, hash::Hash, io, mem, time::{Duration, UNIX_EPOCH}}; + +use windows::{Win32::UI::Shell::*, core::PCWSTR}; +use yazi_shim::Twox128; + +use super::shell_item::ShellItem; +use crate::cha::Cha; + +pub(super) struct TrashSig { + names: Option>, + count_size: Option<(i64, i64)>, +} + +impl TrashSig { + pub(super) fn root() -> io::Result { + let root = ShellItem::root()?; + let cha = root.cha()?; + + let mut info = + SHQUERYRBINFO { cbSize: mem::size_of::() as u32, ..Default::default() }; + + let sig = if unsafe { SHQueryRecycleBinW(PCWSTR::null(), &mut info) }.is_ok() { + Self { names: None, count_size: Some((info.i64NumItems, info.i64Size)) } + } else if cha.mtime.is_some() { + Self { names: None, count_size: None } + } else { + Self { names: Some(Self::names(&root)?), count_size: None } + }; + + Ok(sig.into_cha(cha)) + } + + pub(super) fn item(item: &ShellItem) -> io::Result { + let cha = item.cha()?; + let sig = if cha.mtime.is_none() && cha.is_dir() { + Self { names: Some(Self::names(item)?), count_size: None } + } else { + Self { names: None, count_size: None } + }; + + Ok(sig.into_cha(cha)) + } + + fn names(item: &ShellItem) -> io::Result> { + let mut names: Vec<_> = item + .children()? + .into_iter() + .map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)) + .collect::>()?; + names.sort_unstable(); + Ok(names) + } + + fn into_cha(self, mut cha: Cha) -> Cha { + let mut h = Twox128::default(); + if let Some((count, size)) = self.count_size { + (size, count).hash(&mut h); + } else if let Some(names) = self.names { + names.hash(&mut h); + } else { + return cha; + } + + let hash = h.finish_128(); + cha.ctime = UNIX_EPOCH.checked_add(Duration::from_nanos(hash as u64 ^ (hash >> 64) as u64)); + cha + } +} diff --git a/yazi-plugin/preset/plugins/trash.lua b/yazi-plugin/preset/plugins/trash.lua index 79c3e4a8..9c713c49 100644 --- a/yazi-plugin/preset/plugins/trash.lua +++ b/yazi-plugin/preset/plugins/trash.lua @@ -7,18 +7,43 @@ local function top(url) return url.name and url or nil end -local function node(url) +local function entry(url) local top = top(url) if not top then - return + return -- We are at the root, no entry to return end - return { - key = url.spec.domain, + return fs.trash.entry { top = top.spec.domain, rel = url:strip_prefix(top) or Path.os(""), } end +local function file(url, ent) + return File { + url = url, + cha = ent.cha, + link_to = ent.link_to, + backing = ent.backing, + } +end + +local function files(parent, ents) + for i, ent in ipairs(ents) do + local url = parent:join(Path.os(ent.name)):into_domain(ent.key) + ents[i] = file(url, ent) + end + return ents +end + +local function notify(action, err) + ya.notify { + title = "Trash", + content = string.format("Failed to %s: %s", action, err), + level = "error", + timeout = 10, + } +end + local function absolute(url) if url.is_absolute then return fs.clean_url(url) @@ -36,44 +61,72 @@ local function absolute(url) return fs.clean_url(url:join(root:join(url.path))) end -local function file(url, entry) - entry.url = url - return File(entry) -end - -local function files(parent, entries) - for i, entry in ipairs(entries) do - local url = parent:join(Path.os(entry.name)):into_domain(entry.key) - entries[i] = file(url, entry) +local restore_recursively +local function restore(ent) + local id = { top = ent.top, rel = ent.rel } + local ok, err = fs.trash.restore { ent } + if ok then + return true, 0 + elseif err.kind ~= "AlreadyExists" then + return false, 1, err end - return entries + return restore_recursively(id, err) end -local function notify(action, err) - ya.notify { - title = "Trash", - content = string.format("Failed to %s: %s", action, err), - level = "error", - timeout = 10, - } +restore_recursively = function(id, collision) + local ent, err = fs.trash.entry(id) + if not ent then + return false, 1, err + elseif not ent.cha.is_dir or ent.cha.is_indirect then + return false, 1, collision + end + + local target = ent.original and fs.cha(Url(ent.original), false) + if not target or not target.is_dir or target.is_indirect then + return false, 1, collision + end + + local ents, err = fs.trash.list(fs.trash.entry(ent)) + if not ents then + return false, 1, err + end + + local failed, changed, first = 0, false, nil + for _, child in ipairs(ents) do + local a, b, c = restore(child) + changed, failed, first = changed or a, failed + b, first or c + end + + if failed ~= 0 then + return changed, failed, first + end + + local ok, err = fs.trash.remove("dir", ent) + return changed or ok, ok and 0 or 1, err end function M:setup() ps.sub_remote("trash-restore", function(args) ya.async(function() - local nodes = {} + local ents, err = {}, nil for i, arg in ipairs(args) do - nodes[i] = node(Url(arg)) - if not nodes[i] then - return notify("restore", "Cannot restore the trash root") + ents[i], err = entry(Url(arg)) + if not ents[i] then + return notify("restore", err) end end - local ok, err = fs.trash.restore(nodes) - if ok then + local changed, failed, first = false, 0, nil + for _, ent in ipairs(ents) do + local a, b, c = restore(ent) + changed, failed, first = changed or a, failed + b, first or c + end + + if changed then ya.emit("refresh", {}) - else - notify("restore", err) + end + if failed ~= 0 then + notify("restore", string.format("%d item(s) could not be restored: %s", failed, first)) end end) end) @@ -115,36 +168,46 @@ function M:provide(job) elseif op == "Casefold" then return job.url elseif op == "Metadata" or op == "SymlinkMetadata" then - local n = node(job.url) - if n then - return fs.trash.metadata(n, op == "Metadata") + local ent, err = entry(job.url) + if ent then + return fs.trash.metadata(ent, op == "Metadata") + elseif err then + return nil, err else return Cha { mode = tonumber("40700", 8) } end elseif op == "ReadDir" then - local entries, err = fs.trash.list(node(job.url)) - if entries then - return files(job.url, entries) + local ent, err = entry(job.url) + if err then + return nil, err + end + local ents, err = fs.trash.list(ent) + if ents then + return files(job.url, ents) else return nil, err end elseif op == "Revalidate" then - return fs.trash.revalidate(node(job.file.url), job.file) - elseif op == "File" then - local n = node(job.url) - if not n then - return nil, Error.custom("Cannot construct a file for the trash root") + local ent, err = entry(job.file.url) + if err then + return nil, err + else + return fs.trash.revalidate(ent, job.file) end - local entry, err = fs.trash.entry(n) - if entry then - return file(job.url, entry) + elseif op == "File" then + local ent, err = entry(job.url) + if ent then + return file(job.url, ent) else return nil, err end - elseif op == "RemoveFile" then - return fs.trash.remove("file", node(job.url)) - elseif op == "RemoveDir" then - return fs.trash.remove("dir", node(job.url)) + elseif op == "RemoveFile" or op == "RemoveDir" then + local ent, err = entry(job.url) + if ent then + return fs.trash.remove(op == "RemoveDir" and "dir" or "file", ent) + else + return false, err + end end return false, Error.custom("Unsupported trash operation: " .. op) diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 5b5a59c9..2525274f 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -43,7 +43,7 @@ impl File { _ => Ok(()), }, async |task, cha| { - Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) { + Ok(if cha.is_orphan() || (cha.is_indirect() && !task.follow) { self.ops.out(id, FileOutCopy::New(0)); self.requeue(task.into_link(), NORMAL); } else { @@ -115,7 +115,7 @@ impl File { _ => Ok(()), }, |task, cha| { - let nofollow = cha.is_orphan() || (cha.is_link() && !task.follow); + let nofollow = cha.is_orphan() || (cha.is_indirect() && !task.follow); self.ops.out(id, FileOutCut::New(if nofollow { 0 } else { cha.len })); if nofollow { @@ -196,6 +196,7 @@ impl File { } pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { + let mut cha = task.cha; let mut src: PathCow = if task.resolve { ok_or_not_found!( task, @@ -216,17 +217,29 @@ impl File { ctx!( task, engine::symlink(&task.to, src, async || { - Ok(match task.cha { + Ok(match cha { Some(cha) => cha.is_dir(), - None => Self::cha(&task.from, task.resolve, None).await?.is_dir(), + None => { + cha = Some(Self::cha(&task.from, task.resolve, None).await?); + cha.unwrap().is_dir() + } }) }) .await )?; if task.delete { - engine::remove_file(&task.from).await.ok(); + let cha = match cha { + Some(cha) => cha, + None => ctx!(task, Self::cha(&task.from, task.resolve, None).await)?, + }; + if cha.is_dir() && cha.is_indirect() { + engine::remove_dir(&task.from).await.ok(); + } else { + engine::remove_file(&task.from).await.ok(); + } } + Ok(self.ops.out(task.id, FileOutLink::Succ)) } @@ -291,13 +304,20 @@ impl File { } pub(crate) async fn delete_do(&self, task: FileInDelete) -> Result<(), FileOutDeleteDo> { - match engine::remove_file(&task.target).await { + let cha = task.cha.unwrap(); + let result = if cha.is_dir() && cha.is_indirect() { + engine::remove_dir(&task.target).await + } else { + engine::remove_file(&task.target).await + }; + + match result { Ok(()) => {} Err(e) if e.kind() == NotFound => {} Err(_) if !maybe_exists(&task.target).await => {} Err(e) => ctx!(task, Err(e))?, } - Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(task.cha.unwrap().len))) + Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(cha.len))) } pub(crate) async fn trash(&self, task: FileInTrash) -> Result<(), FileOutTrash> { diff --git a/yazi-scheduler/src/file/transaction.rs b/yazi-scheduler/src/file/transaction.rs index ddf7db60..29d0f3a7 100644 --- a/yazi-scheduler/src/file/transaction.rs +++ b/yazi-scheduler/src/file/transaction.rs @@ -34,7 +34,7 @@ impl Transaction { let url = url.as_url(); let cha = ok_or_not_found!(engine::symlink_metadata(url).await, return Ok(())); - if cha.is_link() { + if cha.is_indirect() { engine::rename(Self::tmp(url).await?, url).await?; } else if !cha.contains(ChaMode::U_WRITE) { engine::set_attrs(url, Attrs::mode(cha.mode | ChaMode::U_WRITE)).await?; diff --git a/yazi-scheduler/src/file/traverse.rs b/yazi-scheduler/src/file/traverse.rs index fab4361c..a99d23c1 100644 --- a/yazi-scheduler/src/file/traverse.rs +++ b/yazi-scheduler/src/file/traverse.rs @@ -159,7 +159,8 @@ where E: Fn(String), { let cha = ctx!(task, task.init().await)?; - if !cha.is_dir() { + let follow_symlink = cha.is_link() && task.follow(); + if !cha.is_dir() || (!follow_symlink && cha.is_indirect()) { return on_file(task, cha).await; } @@ -198,7 +199,8 @@ where "Cannot get metadata for {from:?}" ); - if cha.is_dir() { + let follow_symlink = cha.is_link() && task.follow(); + if cha.is_dir() && (follow_symlink || !cha.is_indirect()) { dirs.push_back(from); continue; } diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index 00216569..57271a9d 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -1,4 +1,4 @@ -use std::{fmt, hash::{Hash, Hasher}, sync::Arc}; +use std::{fmt, sync::Arc}; use serde::Deserialize; use yazi_shim::cell::RoCell; @@ -7,7 +7,7 @@ use crate::{auth::{AuthInventory, AuthKind, Domain, EncodeAuth, Scheme}, path::{ pub(super) static DEFAULT_ARC: RoCell> = RoCell::new(); -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)] pub struct Auth { pub kind: AuthKind, pub scheme: Scheme, @@ -16,22 +16,6 @@ pub struct Auth { pub parent: Option>, } -impl PartialEq for Auth { - fn eq(&self, other: &Self) -> bool { - self.kind == other.kind && self.scheme == other.scheme && self.domain == other.domain - } -} - -impl Eq for Auth {} - -impl Hash for Auth { - fn hash(&self, state: &mut H) { - self.kind.hash(state); - self.scheme.hash(state); - self.domain.hash(state); - } -} - impl Default for Auth { fn default() -> Self { Self::DEFAULT } } diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 70815f9f..f23d8265 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -22,6 +22,10 @@ impl From for PathBufDyn { fn from(value: std::path::PathBuf) -> Self { Self::Os(value) } } +impl From<&std::path::PathBuf> for PathBufDyn { + fn from(value: &std::path::PathBuf) -> Self { Self::Os(value.clone()) } +} + impl From for PathBufDyn { fn from(value: typed_path::UnixPathBuf) -> Self { Self::Unix(value) } } diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index 16fdf6f1..7d4ce8f9 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -92,14 +92,19 @@ impl<'a> Strand<'a> { } } - #[inline] - pub fn to_os_string(self) -> Result { self.as_os().map(|s| s.to_owned()) } - #[inline] pub fn as_os_path(self) -> Result<&'a std::path::Path, StrandError> { self.as_os().map(std::path::Path::new) } + #[inline] + pub fn to_os_path(self) -> Result { + self.as_os().map(std::path::PathBuf::from) + } + + #[inline] + pub fn to_os_string(self) -> Result { self.as_os().map(|s| s.to_owned()) } + #[inline] pub fn as_unix_path(self) -> &'a typed_path::UnixPath { typed_path::UnixPath::new(self.encoded_bytes()) diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index e36bd485..a0a021e6 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -22,7 +22,6 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" } # External dependencies anyhow = { workspace = true } arc-swap = { workspace = true } -base64 = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } percent-encoding = { workspace = true } diff --git a/yazi-shim/src/base64.rs b/yazi-shim/src/base64.rs deleted file mode 100644 index c3f6dae0..00000000 --- a/yazi-shim/src/base64.rs +++ /dev/null @@ -1,8 +0,0 @@ -use base64::{alphabet::STANDARD, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}}; - -pub const BASE64_SANE: GeneralPurpose = GeneralPurpose::new( - &STANDARD, - GeneralPurposeConfig::new() - .with_encode_padding(false) - .with_decode_padding_mode(DecodePaddingMode::Indifferent), -); diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index c5dabb9b..e9e28c5e 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(arc_swap cell fs mlua path ratatui serde strum toml vec wtf8); -yazi_macro::mod_flat!(base64 option percent_encoding sstr twox utf8); +yazi_macro::mod_flat!(option percent_encoding sstr twox utf8); #[cfg(windows)] yazi_macro::mod_flat!(win32); diff --git a/yazi-shim/src/path/mod.rs b/yazi-shim/src/path/mod.rs index 2122106c..15b23599 100644 --- a/yazi-shim/src/path/mod.rs +++ b/yazi-shim/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(separator); +yazi_macro::mod_flat!(separator traits); diff --git a/yazi-shim/src/path/traits.rs b/yazi-shim/src/path/traits.rs new file mode 100644 index 00000000..74378e49 --- /dev/null +++ b/yazi-shim/src/path/traits.rs @@ -0,0 +1,11 @@ +use std::path::{Component, Path}; + +pub trait PathExt { + fn has_parent_component(&self) -> bool; +} + +impl PathExt for Path { + fn has_parent_component(&self) -> bool { + self.components().any(|c| matches!(c, Component::ParentDir)) + } +} diff --git a/yazi-term/src/event/dnd.rs b/yazi-term/src/event/dnd.rs index d3c81b6a..fa82db5e 100644 --- a/yazi-term/src/event/dnd.rs +++ b/yazi-term/src/event/dnd.rs @@ -1,8 +1,7 @@ use std::str::SplitWhitespace; -use base64::Engine; +use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD_INDIFFERENT}; use strum::{FromRepr, IntoStaticStr}; -use yazi_shim::BASE64_SANE; use crate::parser::StateOsc72; @@ -195,7 +194,7 @@ impl DndEvent { }), b'r' => Self::DropArrive(DndDropArrive { idx: s.x?.try_into().ok()?, - data: BASE64_SANE.decode(&s.payload).ok()?, + data: STANDARD_NO_PAD_INDIFFERENT.decode(&s.payload).ok()?, }), b'R' => { let (name, desc) = parse_error(s.payload)?; diff --git a/yazi-tty/src/sequence/dnd.rs b/yazi-tty/src/sequence/dnd.rs index 9591ff17..989bed76 100644 --- a/yazi-tty/src/sequence/dnd.rs +++ b/yazi-tty/src/sequence/dnd.rs @@ -1,7 +1,6 @@ use std::{fmt::{self, Display}, str}; -use base64::Engine; -use yazi_shim::BASE64_SANE; +use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD_INDIFFERENT}; use super::traits::Mimelist; @@ -92,7 +91,7 @@ pub struct PresentDrag<'a>(pub u8, pub &'a [u8]); impl Display for PresentDrag<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let b64 = BASE64_SANE.encode(self.1).into_bytes(); + let b64 = STANDARD_NO_PAD_INDIFFERENT.encode(self.1).into_bytes(); let chunks = b64.len().div_ceil(4096); for (i, chunk) in b64.chunks(4096).enumerate() { @@ -120,7 +119,7 @@ pub struct PresentDragIcon<'a> { impl Display for PresentDragIcon<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let b64 = BASE64_SANE.encode(self.data).into_bytes(); + let b64 = STANDARD_NO_PAD_INDIFFERENT.encode(self.data).into_bytes(); let chunks = b64.len().div_ceil(4096); for (i, chunk) in b64.chunks(4096).enumerate() { diff --git a/yazi-tui/src/raterm.rs b/yazi-tui/src/raterm.rs index 4908d59c..7dd6f566 100644 --- a/yazi-tui/src/raterm.rs +++ b/yazi-tui/src/raterm.rs @@ -48,7 +48,8 @@ impl Raterm { If(TMUX.get(), EnterAlternateScreen), PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES | PushKeyboardFlags::REPORT_ALTERNATE_KEYS - | PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES, + | PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES + | PushKeyboardFlags::REPORT_ASSOCIATED_TEXT, EnableDrag(""), EnableDrop(&["text/uri-list"]), If(opt.mouse, EnableMouseCapture), diff --git a/yazi-vfs/src/engine/calculator.rs b/yazi-vfs/src/engine/calculator.rs index bf5cfa5d..71d6cfeb 100644 --- a/yazi-vfs/src/engine/calculator.rs +++ b/yazi-vfs/src/engine/calculator.rs @@ -18,7 +18,7 @@ impl SizeCalculator { { let url = url.as_url(); let cha = super::symlink_metadata(url).await?; - Ok(if cha.is_dir() { + Ok(if cha.is_dir() && !cha.is_indirect() { Self::Dir(VecDeque::from([Either::Left(url.to_owned())]), cha) } else { Self::File(Some(cha.len), cha) @@ -77,10 +77,10 @@ impl SizeCalculator { pop_and_continue!(); }; - let Ok(ft) = ent.file_type().await else { continue }; - if ft.is_dir() { + let Ok(cha) = ent.metadata().await else { continue }; + if cha.is_dir() && !cha.is_indirect() { buf.push_back(Either::Left(ent.url())); - } else if let Ok(cha) = ent.metadata().await { + } else { size += cha.len; } } diff --git a/yazi-vfs/src/engine/engines.rs b/yazi-vfs/src/engine/engines.rs index e948ebcf..3974758c 100644 --- a/yazi-vfs/src/engine/engines.rs +++ b/yazi-vfs/src/engine/engines.rs @@ -193,6 +193,14 @@ impl<'a> Engine for Engines<'a> { } } + async fn remove_dir_clean(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_dir_clean().await, + Self::Lua(p) => p.remove_dir_clean().await, + Self::Sftp(p) => p.remove_dir_clean().await, + } + } + async fn remove_file(&self) -> io::Result<()> { match self { Self::Local(p) => p.remove_file().await,