feat!: show file icons in trash/delete/overwrite confirmations (#4096)

This commit is contained in:
三咲雅 misaki masa 2026-07-02 02:57:12 +08:00 committed by sxyazi
parent 21550a7eb5
commit b8ceaea940
No known key found for this signature in database
83 changed files with 779 additions and 439 deletions

View file

@ -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]) - Make help menu a command palette ([#4074])
- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970])
- Context-aware icons for inputs ([#4080]) - Context-aware icons for inputs ([#4080])
- Show file icons in trash/delete/overwrite confirmations ([#4096])
- Dynamic keymap Lua API ([#4031]) - Dynamic keymap Lua API ([#4031])
- New `ui.Input` element ([#4040]) - New `ui.Input` element ([#4040])
- Image preview with Überzug++ on Niri ([#3990]) - Image preview with Überzug++ on Niri ([#3990])
@ -27,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Changed ### Changed
- Rename `<BackTab>` to `<S-Tab>` ([#3989]) - Rename `<BackTab>` to `<S-Tab>` ([#3989])
- 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]) - 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]) - `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074])
- Remove Legacy Console Mode on Windows ([#3989]) - Remove Legacy Console Mode on Windows ([#3989])
@ -1765,3 +1767,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#4068]: https://github.com/sxyazi/yazi/pull/4068 [#4068]: https://github.com/sxyazi/yazi/pull/4068
[#4074]: https://github.com/sxyazi/yazi/pull/4074 [#4074]: https://github.com/sxyazi/yazi/pull/4074
[#4080]: https://github.com/sxyazi/yazi/pull/4080 [#4080]: https://github.com/sxyazi/yazi/pull/4080
[#4096]: https://github.com/sxyazi/yazi/pull/4096

31
Cargo.lock generated
View file

@ -41,9 +41,9 @@ dependencies = [
[[package]] [[package]]
name = "aes-gcm" name = "aes-gcm"
version = "0.11.0-rc.4" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028"
dependencies = [ dependencies = [
"aead", "aead",
"aes", "aes",
@ -182,9 +182,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.9.1" version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [ dependencies = [
"rustversion", "rustversion",
"serde", "serde",
@ -628,9 +628,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.6.5" version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" checksum = "97bf4965940c2382204c0ded6dd3dd48c0c4e872f1e76fb1bf94f45991a2cb6a"
dependencies = [ dependencies = [
"clap", "clap",
] ]
@ -1583,9 +1583,9 @@ dependencies = [
[[package]] [[package]]
name = "hybrid-array" name = "hybrid-array"
version = "0.4.12" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [ dependencies = [
"ctutils", "ctutils",
"subtle", "subtle",
@ -2556,11 +2556,12 @@ dependencies = [
[[package]] [[package]]
name = "pkcs5" name = "pkcs5"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca"
dependencies = [ dependencies = [
"aes", "aes",
"aes-gcm",
"cbc", "cbc",
"der", "der",
"pbkdf2", "pbkdf2",
@ -3831,9 +3832,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.51" version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
dependencies = [ dependencies = [
"deranged", "deranged",
"libc", "libc",
@ -3853,9 +3854,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.30" version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@ -4704,6 +4705,7 @@ dependencies = [
"futures", "futures",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"indexmap 2.14.0", "indexmap 2.14.0",
"inventory",
"libc", "libc",
"mlua", "mlua",
"paste", "paste",
@ -5222,6 +5224,7 @@ dependencies = [
"foldhash", "foldhash",
"futures", "futures",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"indexmap 2.14.0",
"inventory", "inventory",
"libc", "libc",
"memchr", "memchr",

View file

@ -37,7 +37,7 @@ debug = false
[workspace.dependencies] [workspace.dependencies]
ansi-to-tui = "8.0.1" ansi-to-tui = "8.0.1"
anyhow = "1.0.103" anyhow = "1.0.103"
arc-swap = { version = "1.9.1", features = [ "serde" ] } arc-swap = { version = "1.9.2", features = [ "serde" ] }
base64 = "0.22.1" base64 = "0.22.1"
bitflags = { version = "2.13.0", features = [ "serde" ] } bitflags = { version = "2.13.0", features = [ "serde" ] }
chrono = "0.4.45" chrono = "0.4.45"

View file

@ -45,6 +45,7 @@ either = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
ratatui-core = { workspace = true } ratatui-core = { workspace = true }

View file

@ -5,19 +5,19 @@ use yazi_shim::mlua::UserDataFieldsExt;
use super::{File, Filter, Lives, PtrCell}; use super::{File, Filter, Lives, PtrCell};
pub(super) struct Files { pub(super) struct Entries {
window: Range<usize>, window: Range<usize>,
folder: PtrCell<yazi_core::tab::Folder>, folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
} }
impl Deref for Files { impl Deref for Entries {
type Target = yazi_fs::Files; type Target = yazi_fs::Entries;
fn deref(&self) -> &Self::Target { &self.folder.files } fn deref(&self) -> &Self::Target { &self.folder.entries }
} }
impl Files { impl Entries {
pub(super) fn make( pub(super) fn make(
window: Range<usize>, window: Range<usize>,
folder: &yazi_core::tab::Folder, folder: &yazi_core::tab::Folder,
@ -27,7 +27,7 @@ impl Files {
} }
} }
impl UserData for Files { impl UserData for Entries {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose()); fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose());
} }

View file

@ -3,6 +3,7 @@ use std::{ops::Deref, ptr};
use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods}; use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Range, style::Style}; use yazi_binding::{Range, style::Style};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_fs::file::FileInventory;
use yazi_shared::{path::AsPath, url::UrlLike}; use yazi_shared::{path::AsPath, url::UrlLike};
use super::{FILE_CACHE, Lives}; use super::{FILE_CACHE, Lives};
@ -17,7 +18,7 @@ pub(super) struct File {
impl Deref for File { impl Deref for File {
type Target = yazi_fs::file::File; type Target = yazi_fs::file::File;
fn deref(&self) -> &Self::Target { &self.folder.files[self.idx] } fn deref(&self) -> &Self::Target { &self.folder.entries[self.idx] }
} }
impl AsRef<yazi_fs::file::File> for File { impl AsRef<yazi_fs::file::File> for File {
@ -32,14 +33,16 @@ impl File {
) -> mlua::Result<AnyUserData> { ) -> mlua::Result<AnyUserData> {
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
Ok(match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.files[idx])) { Ok(
Entry::Occupied(oe) => oe.into_mut().clone(), match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.entries[idx])) {
Entry::Vacant(ve) => { Entry::Occupied(oe) => oe.into_mut().clone(),
let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?; Entry::Vacant(ve) => {
ve.insert(ud.clone()); let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?;
ud ve.insert(ud.clone());
} ud
}) }
},
)
} }
#[inline] #[inline]
@ -49,7 +52,6 @@ impl File {
impl UserData for File { impl UserData for File {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
yazi_binding::impl_file_fields!(fields); yazi_binding::impl_file_fields!(fields);
fields.add_cached_field("bare", |_, me| Ok(yazi_fs::file::File::from(&**me)));
fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1));
fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered())); fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered()));
@ -67,7 +69,7 @@ impl UserData for File {
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered())) Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
}); });
methods.add_method("size", |_, me, ()| { methods.add_method("size", |_, me, ()| {
Ok(if me.is_dir() { me.folder.files.sizes.get(&me.urn()).copied() } else { Some(me.len) }) Ok(if me.is_dir() { me.folder.entries.sizes.get(&me.urn()).copied() } else { Some(me.len) })
}); });
methods.add_method("mime", |lua, me, ()| { methods.add_method("mime", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?; let core: CoreRef = lua.named_registry_value("cx")?;
@ -138,3 +140,10 @@ impl UserData for File {
}); });
} }
} }
inventory::submit! {
FileInventory {
register: |_| {},
from_lua: |ud| Ok(ud.borrow::<File>()?.clone()),
}
}

View file

@ -4,7 +4,7 @@ use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_shim::mlua::UserDataFieldsExt; use yazi_shim::mlua::UserDataFieldsExt;
use super::{File, Files, Lives, PtrCell}; use super::{Entries, File, Lives, PtrCell};
pub(super) struct Folder { pub(super) struct Folder {
window: Range<usize>, window: Range<usize>,
@ -28,7 +28,7 @@ impl Folder {
Some(w) => w, Some(w) => w,
None => { None => {
let limit = LAYOUT.get().preview.height as usize; let limit = LAYOUT.get().preview.height as usize;
inner.offset..inner.files.len().min(inner.offset + limit) inner.offset..inner.entries.len().min(inner.offset + limit)
} }
}; };
@ -39,9 +39,9 @@ impl Folder {
impl UserData for Folder { impl UserData for Folder {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("cwd", |_, me| Ok(me.url.clone())); fields.add_cached_field("cwd", |_, me| Ok(me.url.clone()));
fields.add_static_field("files", |_, me| Files::make(0..me.files.len(), me, &me.tab)); fields.add_static_field("files", |_, me| Entries::make(0..me.entries.len(), me, &me.tab));
fields.add_cached_field("stage", |_, me| Ok(me.stage.clone())); fields.add_cached_field("stage", |_, me| Ok(me.stage.clone()));
fields.add_static_field("window", |_, me| Files::make(me.window.clone(), me, &me.tab)); fields.add_static_field("window", |_, me| Entries::make(me.window.clone(), me, &me.tab));
fields.add_field_method_get("offset", |_, me| Ok(me.offset)); fields.add_field_method_get("offset", |_, me| Ok(me.offset));
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));

View file

@ -1,4 +1,4 @@
yazi_macro::mod_flat!(behavior core file files filter finder folder input input_alt lives mode mut_cell preference preview ptr selected tab tabs task tasks which yanked); yazi_macro::mod_flat!(behavior core entries file filter finder folder input input_alt lives mode mut_cell preference preview ptr selected tab tabs task tasks which yanked);
pub(super) fn init() { pub(super) fn init() {
unsafe { FILE_CACHE.get().write(std::mem::MaybeUninit::new(<_>::default())) }; unsafe { FILE_CACHE.get().write(std::mem::MaybeUninit::new(<_>::default())) };

View file

@ -30,7 +30,7 @@ impl UserData for Preview {
.hovered_folder() .hovered_folder()
.map(|f| { .map(|f| {
let limit = LAYOUT.get().preview.height as usize; let limit = LAYOUT.get().preview.height as usize;
Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, &me.tab) Folder::make(Some(me.skip..f.entries.len().min(me.skip + limit)), f, &me.tab)
}) })
.transpose() .transpose()
}); });

View file

@ -1,8 +1,6 @@
use mlua::AnyUserData; use mlua::AnyUserData;
use yazi_shared::url::UrlBuf;
use super::Lives; use super::{Lives, PtrCell};
use crate::lives::PtrCell;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(super) struct Selected; pub(super) struct Selected;
@ -12,7 +10,7 @@ impl Selected {
let inner = PtrCell::from(inner); let inner = PtrCell::from(inner);
Lives::scoped_userdata(yazi_binding::Iter::new( Lives::scoped_userdata(yazi_binding::Iter::new(
inner.as_static().values().map(UrlBuf::from), inner.as_static().files().cloned(),
Some(inner.len()), Some(inner.len()),
)) ))
} }

View file

@ -2,7 +2,7 @@ use std::ops::Deref;
use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods}; use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods};
use yazi_binding::Iter; use yazi_binding::Iter;
use yazi_shared::url::UrlBuf; use yazi_fs::file::File;
use yazi_shim::mlua::get_metatable; use yazi_shim::mlua::get_metatable;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
@ -25,7 +25,7 @@ impl Yanked {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self {
inner, inner,
iter: Lives::scoped_userdata(Iter::new( iter: Lives::scoped_userdata(Iter::new(
inner.as_static().iter().map(UrlBuf::from), inner.as_static().iter().map(File::from),
Some(inner.len()), Some(inner.len()),
))?, ))?,
}) })

View file

@ -32,7 +32,7 @@ impl Actor for BulkRename {
succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found")); succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found"));
}; };
let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect(); let selected: Vec<_> = cx.tab().selected_or_hovered_urls().cloned().collect();
if selected.is_empty() { if selected.is_empty() {
succ!(NotifyProxy::push_warn("Bulk rename", "No files selected")); succ!(NotifyProxy::push_warn("Bulk rename", "No files selected"));
} }

View file

@ -20,7 +20,7 @@ impl Actor for Copy {
let mut it = if form.hovered { let mut it = if form.hovered {
Box::new(cx.hovered().map(|h| &h.url).into_iter()) Box::new(cx.hovered().map(|h| &h.url).into_iter())
} else { } else {
cx.tab().selected_or_hovered() cx.tab().selected_or_hovered_urls()
} }
.peekable(); .peekable();

View file

@ -9,7 +9,7 @@ use yazi_macro::{input, ok_or_not_found, succ};
use yazi_parser::mgr::CreateForm; use yazi_parser::mgr::CreateForm;
use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{VfsFile, provider};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -34,22 +34,24 @@ impl Actor for Create {
}; };
tokio::spawn(async move { tokio::spawn(async move {
let Some(name) = target.next().await else { return }; let Some(name) = target.next().await else { return Ok(()) };
if name.is_empty() { if name.is_empty() {
return; return Ok(());
} }
let Ok(new) = cwd.try_join(&name) else { return }; let Ok(new) = cwd.try_join(&name) else {
bail!("Failed to join new name with CWD");
};
if !force if !force
&& maybe_exists(&new).await && let Some(file) = File::maybe_new(&new).await?
&& !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await && !ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await
{ {
return; return Ok(());
} }
let end_sep = AnyAsciiChar::SEP.predicate(*name.encoded_bytes().last().unwrap()); let end_sep = AnyAsciiChar::SEP.predicate(*name.encoded_bytes().last().unwrap());
_ = Self::r#do(new, dir || end_sep).await; Self::r#do(new, dir || end_sep).await
}); });
succ!(); succ!();
} }

View file

@ -70,12 +70,11 @@ impl Actor for EscapeVisual {
let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) }; let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) };
render!(); render!();
let urls: Vec<_> = let files: Vec<_> = indices.into_iter().filter_map(|i| tab.current.entries.get(i)).collect();
indices.into_iter().filter_map(|i| tab.current.files.get(i)).map(|f| &f.url).collect();
if !select { if !select {
tab.selected.remove_many(urls); tab.selected.remove_many(files);
} else if urls.len() != tab.selected.add_many(urls) { } else if files.len() != tab.selected.add_many(files) {
NotifyProxy::push_warn( NotifyProxy::push_warn(
"Escape visual mode", "Escape visual mode",
"Some files cannot be selected, due to path nesting conflict.", "Some files cannot be selected, due to path nesting conflict.",
@ -96,7 +95,7 @@ impl Actor for EscapeFilter {
const NAME: &str = "escape_filter"; const NAME: &str = "escape_filter";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if cx.current_mut().files.filter().is_none() { if cx.current_mut().entries.filter().is_none() {
succ!(false); succ!(false);
} }

View file

@ -17,7 +17,7 @@ impl Actor for FilterDo {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; 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.urn().into());
cx.current_mut().files.set_filter(filter); 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.urn()) != hovered.as_ref().map(Into::into) {
act!(mgr:hover, cx, hovered)?; act!(mgr:hover, cx, hovered)?;

View file

@ -18,9 +18,9 @@ impl Actor for FindArrow {
render!(finder.catchup(&tab.current)); render!(finder.catchup(&tab.current));
let offset = if form.prev { let offset = if form.prev {
finder.prev(&tab.current.files, tab.current.cursor, false) finder.prev(&tab.current.entries, tab.current.cursor, false)
} else { } else {
finder.next(&tab.current.files, tab.current.cursor, false) finder.next(&tab.current.entries, tab.current.cursor, false)
}; };
if let Some(offset) = offset { if let Some(offset) = offset {

View file

@ -24,9 +24,9 @@ impl Actor for FindDo {
} }
let step = if opt.prev { let step = if opt.prev {
finder.prev(&cx.current().files, cx.current().cursor, true) finder.prev(&cx.current().entries, cx.current().cursor, true)
} else { } else {
finder.next(&cx.current().files, cx.current().cursor, true) finder.next(&cx.current().entries, cx.current().cursor, true)
}; };
if let Some(step) = step { if let Some(step) = step {

View file

@ -24,8 +24,8 @@ impl Actor for Hidden {
render!(); render!();
false false
} else { } else {
f.files.set_show_hidden(state); f.entries.set_show_hidden(state);
render_and!(f.files.catchup_revision()) render_and!(f.entries.catchup_revision())
} }
}; };

View file

@ -33,6 +33,7 @@ yazi_macro::mod_flat!(
quit quit
refresh refresh
remove remove
remove_do
rename rename
reveal reveal
search search

View file

@ -27,7 +27,7 @@ impl Actor for Open {
Quit::with_selected(cx.hovered().map(|h| &h.url)) Quit::with_selected(cx.hovered().map(|h| &h.url))
} else { } else {
act!(mgr:escape_visual, cx)?; act!(mgr:escape_visual, cx)?;
Quit::with_selected(cx.tab().selected_or_hovered()) Quit::with_selected(cx.tab().selected_or_hovered_urls())
}); });
} }
@ -36,7 +36,7 @@ impl Actor for Open {
cx.hovered().map(|h| vec![h.url.clone()]).unwrap_or_default() cx.hovered().map(|h| vec![h.url.clone()]).unwrap_or_default()
} else { } else {
act!(mgr:escape_visual, cx)?; act!(mgr:escape_visual, cx)?;
cx.tab().selected_or_hovered().cloned().collect() cx.tab().selected_or_hovered_urls().cloned().collect()
}; };
} }
if opt.targets.is_empty() { if opt.targets.is_empty() {

View file

@ -20,7 +20,7 @@ impl Actor for Paste {
if mgr.yanked.cut { if mgr.yanked.cut {
cx.core.tasks.file_cut(&mgr.yanked, dest, form.force); cx.core.tasks.file_cut(&mgr.yanked, dest, form.force);
mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(&*mgr.yanked)); mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(mgr.yanked.urls()));
act!(mgr:unyank, cx) act!(mgr:unyank, cx)
} else { } else {
succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow)); succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow));

View file

@ -1,10 +1,10 @@
use anyhow::Result; use anyhow::Result;
use yazi_core::tab::Folder; use yazi_core::tab::Folder;
use yazi_fs::{CWD, Files, FilesOp, cha::Cha}; use yazi_fs::{CWD, Entries, FilesOp, cha::Cha};
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::VoidForm; use yazi_parser::VoidForm;
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFiles, VfsFilesOp}; use yazi_vfs::{VfsEntries, VfsFilesOp};
use yazi_watcher::MgrProxy; use yazi_watcher::MgrProxy;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -29,7 +29,7 @@ impl Actor for Refresh {
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?; act!(mgr:update_paged, cx)?;
cx.tasks.prework_sorted(&cx.current().files); cx.tasks.prework_sorted(&cx.current().entries);
succ!(); succ!();
} }
} }
@ -44,9 +44,9 @@ impl Refresh {
// TODO: performance improvement // TODO: performance improvement
fn trigger_dirs(folders: &[&Folder]) { fn trigger_dirs(folders: &[&Folder]) {
async fn go(dir: UrlBuf, cha: Cha) { async fn go(dir: UrlBuf, cha: Cha) {
let Some(cha) = Files::assert_stale(&dir, cha).await else { return }; let Some(cha) = Entries::assert_stale(&dir, cha).await else { return };
match Files::from_dir_bulk(&dir).await { match Entries::from_dir_bulk(&dir).await {
Ok(files) => FilesOp::Full(dir, files, cha).emit(), Ok(files) => FilesOp::Full(dir, files, cha).emit(),
Err(e) => FilesOp::issue_error(&dir, e).await, Err(e) => FilesOp::issue_error(&dir, e).await,
} }

View file

@ -1,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use yazi_config::popup::ConfirmCfg; use yazi_config::popup::ConfirmCfg;
use yazi_fs::file::Files;
use yazi_macro::{act, confirm, succ}; use yazi_macro::{act, confirm, succ};
use yazi_parser::mgr::RemoveForm; use yazi_parser::mgr::{RemoveDoForm, RemoveForm};
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::data::Data; use yazi_shared::data::Data;
@ -14,58 +15,31 @@ impl Actor for Remove {
const NAME: &str = "remove"; const NAME: &str = "remove";
fn act(cx: &mut Ctx, mut form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(mgr:escape_visual, cx)?; act!(mgr:escape_visual, cx)?;
form.targets = if form.hovered { let targets = Files(if form.hovered {
cx.hovered().map_or(vec![], |h| vec![h.url.clone()]) cx.hovered().map_or(vec![], |h| vec![h.clone()])
} else { } else {
cx.tab().selected_or_hovered().cloned().collect() cx.tab().selected_or_hovered_files().cloned().collect()
}; });
if form.targets.is_empty() { if targets.is_empty() {
succ!(); succ!();
} else if form.force { } else if form.force {
return act!(mgr:remove_do, cx, form); return act!(mgr:remove_do, cx, RemoveDoForm { permanently: form.permanently, targets: targets.into() });
} }
let confirm = confirm!( let confirm = confirm!(
cx, cx,
if form.permanently { if form.permanently { ConfirmCfg::delete(&targets) } else { ConfirmCfg::trash(&targets) }
ConfirmCfg::delete(&form.targets)
} else {
ConfirmCfg::trash(&form.targets)
}
)?; )?;
tokio::spawn(async move { tokio::spawn(async move {
if confirm.future().await { if confirm.future().await {
MgrProxy::remove_do(form.targets, form.permanently); MgrProxy::remove_do(form.permanently, targets);
} }
}); });
succ!(); succ!();
} }
} }
// --- Do
pub struct RemoveDo;
impl Actor for RemoveDo {
type Form = RemoveForm;
const NAME: &str = "remove_do";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let mgr = &mut cx.mgr;
mgr.tabs.iter_mut().for_each(|t| {
t.selected.remove_many(&form.targets);
});
mgr.yanked.remove_many(&form.targets);
mgr.yanked.catchup_revision(false);
cx.tasks.file_remove(form.targets, form.permanently);
succ!();
}
}

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::RemoveDoForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct RemoveDo;
impl Actor for RemoveDo {
type Form = RemoveDoForm;
const NAME: &str = "remove_do";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let mgr = &mut cx.mgr;
mgr.tabs.iter_mut().for_each(|t| {
t.selected.remove_many(&form.targets);
});
mgr.yanked.remove_many(&form.targets);
mgr.yanked.catchup_revision(false);
cx.tasks.file_remove(form.targets, form.permanently);
succ!();
}
}

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{Result, bail};
use yazi_config::{YAZI, popup::ConfirmCfg}; use yazi_config::{YAZI, popup::ConfirmCfg};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, file::File}; use yazi_fs::{FilesOp, file::File};
@ -6,7 +6,7 @@ use yazi_macro::{act, err, input, ok_or_not_found, succ};
use yazi_parser::mgr::RenameForm; use yazi_parser::mgr::RenameForm;
use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{data::Data, id::Id, url::{UrlBuf, UrlLike}}; use yazi_shared::{data::Data, id::Id, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{VfsFile, provider};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
use yazi_widgets::input::InputEvent; use yazi_widgets::input::InputEvent;
@ -46,20 +46,19 @@ impl Actor for Rename {
input!(cx, YAZI.input.rename(hovered.is_dir()).with_value(name).with_cursor(cursor))?; input!(cx, YAZI.input.rename(hovered.is_dir()).with_value(name).with_cursor(cursor))?;
tokio::spawn(async move { tokio::spawn(async move {
let Some(InputEvent::Submit(name)) = input.recv().await else { return }; let Some(InputEvent::Submit(name)) = input.recv().await else { return Ok(()) };
if name.is_empty() { if name.is_empty() {
return; return Ok(());
} }
let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else { let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else {
return; bail!("Failed to join new name with parent directory");
}; };
if form.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await { if form.force || Self::try_ask(&old, &new).await? {
Self::r#do(tab, old, new).await.ok(); Self::r#do(tab, old, new).await?;
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
Self::r#do(tab, old, new).await.ok();
} }
Ok::<(), anyhow::Error>(())
}); });
succ!(); succ!();
} }
@ -98,6 +97,17 @@ impl Rename {
Ok(()) Ok(())
} }
async fn try_ask(old: &UrlBuf, new: &UrlBuf) -> Result<bool> {
let Some(file) = File::maybe_new(&new).await? else {
return Ok(true);
};
Ok(
provider::must_identical(old, new).await
|| ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await,
)
}
fn empty_url_part(url: &UrlBuf, by: &str) -> String { fn empty_url_part(url: &UrlBuf, by: &str) -> String {
if by == "all" { if by == "all" {
return String::new(); return String::new();

View file

@ -30,8 +30,8 @@ impl Actor for Sort {
render!(); render!();
false false
} else { } else {
f.files.set_sorter(sorter); f.entries.set_sorter(sorter);
render_and!(f.files.catchup_revision()) render_and!(f.entries.catchup_revision())
} }
}; };
@ -41,7 +41,7 @@ impl Actor for Sort {
{ {
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?; act!(mgr:update_paged, cx)?;
cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.files); cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.entries);
} }
// Apply to hovered // Apply to hovered

View file

@ -15,14 +15,14 @@ impl Actor for Toggle {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let tab = cx.tab_mut(); let tab = cx.tab_mut();
let Some(url) = form.url.or_else(|| tab.hovered().map(|h| h.url.clone())) else { let Some(file) = form.file.or_else(|| tab.hovered().cloned()) else {
succ!(); succ!();
}; };
let b = match form.state { let b = match form.state {
Some(true) => render_and!(tab.selected.add(&url)), Some(true) => render_and!(tab.selected.add(file)),
Some(false) => render_and!(tab.selected.remove(&url)) | true, Some(false) => render_and!(tab.selected.remove(&file.url)) | true,
None => render_and!(tab.selected.remove(&url) || tab.selected.add(&url)), None => render_and!(tab.selected.remove(&file.url) || tab.selected.add(file)),
}; };
if !b { if !b {

View file

@ -17,14 +17,14 @@ impl Actor for ToggleAll {
use either::Either::*; use either::Either::*;
let tab = cx.tab_mut(); let tab = cx.tab_mut();
let it = tab.current.files.iter().map(|f| &f.url); let it = tab.current.entries.iter();
let either = match form.state { let either = match form.state {
Some(true) if form.urls.is_empty() => Left((vec![], it.collect())), Some(true) if form.files.is_empty() => Left((vec![], it.collect())),
Some(true) => Right((vec![], form.urls)), Some(true) => Right((vec![], form.files)),
Some(false) if form.urls.is_empty() => Left((it.collect(), vec![])), Some(false) if form.files.is_empty() => Left((it.collect(), vec![])),
Some(false) => Right((form.urls, vec![])), Some(false) => Right((form.files, vec![])),
None if form.urls.is_empty() => Left(it.partition(|&u| tab.selected.contains(u))), None if form.files.is_empty() => Left(it.partition(|&f| tab.selected.contains(f))),
None => Right(form.urls.into_iter().partition(|u| tab.selected.contains(u))), None => Right(form.files.into_iter().partition(|f| tab.selected.contains(f))),
}; };
let warn = match either { let warn = match either {
@ -34,14 +34,14 @@ impl Actor for ToggleAll {
} }
Right((removal, addition)) => { Right((removal, addition)) => {
render!(tab.selected.remove_many(&removal) > 0); render!(tab.selected.remove_many(&removal) > 0);
render!(tab.selected.add_many(&addition), > 0) != addition.len() addition.len() != render!(tab.selected.add_many(&addition), > 0)
} }
}; };
if warn { if warn {
NotifyProxy::push_warn( NotifyProxy::push_warn(
"Toggle all", "Toggle all",
"Some files cannot be selected, due to path nesting conflict.", "Some files cannot be selected due to path nesting conflict.",
); );
} }
succ!(); succ!();

View file

@ -16,7 +16,7 @@ impl Actor for UpdateFiles {
const NAME: &str = "update_files"; const NAME: &str = "update_files";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let revision = cx.current().files.revision; let revision = cx.current().entries.revision;
let linked: Vec<_> = LINKED.read().from_dir(form.op.cwd()).map(|u| form.op.chdir(u)).collect(); let linked: Vec<_> = LINKED.read().from_dir(form.op.cwd()).map(|u| form.op.chdir(u)).collect();
for op in [form.op].into_iter().chain(linked) { for op in [form.op].into_iter().chain(linked) {
@ -28,7 +28,7 @@ impl Actor for UpdateFiles {
act!(mgr:hidden, cx).ok(); act!(mgr:hidden, cx).ok();
act!(mgr:sort, cx).ok(); act!(mgr:sort, cx).ok();
if revision != cx.current().files.revision { if revision != cx.current().entries.revision {
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
@ -80,7 +80,7 @@ impl UpdateFiles {
} }
if calc { if calc {
cx.tasks.prework_sorted(&cx.current().files); cx.tasks.prework_sorted(&cx.current().entries);
} }
succ!(); succ!();
} }

View file

@ -14,11 +14,11 @@ impl Actor for UpdateYanked {
const NAME: &str = "update_yanked"; const NAME: &str = "update_yanked";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
if form.urls.is_empty() && cx.mgr.yanked.is_empty() { if form.files.is_empty() && cx.mgr.yanked.is_empty() {
succ!(); succ!();
} }
cx.mgr.yanked = Yanked::new(form.cut, form.0.urls.into_owned()); cx.mgr.yanked = Yanked::new(form.cut, form.0.files.into_owned());
succ!(render!()); succ!(render!());
} }
} }

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use yazi_core::mgr::Yanked; use yazi_core::mgr::Yanked;
use yazi_macro::{act, render}; use yazi_macro::{act, render};
use yazi_parser::mgr::YankForm; use yazi_parser::mgr::YankForm;
use yazi_shared::{data::Data, url::UrlBufCov}; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -17,7 +17,7 @@ impl Actor for Yank {
act!(mgr:escape_visual, cx)?; act!(mgr:escape_visual, cx)?;
cx.mgr.yanked = cx.mgr.yanked =
Yanked::new(form.cut, cx.tab().selected_or_hovered().cloned().map(UrlBufCov).collect()); Yanked::new(form.cut, cx.tab().selected_or_hovered_files().map(Into::into).collect());
render!(cx.mgr.yanked.catchup_revision(true)); render!(cx.mgr.yanked.catchup_revision(true));
act!(mgr:escape_select, cx) act!(mgr:escape_select, cx)

View file

@ -30,6 +30,6 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
# External dependencies # External dependencies
clap = { workspace = true } clap = { workspace = true }
clap_complete = "4.6.5" clap_complete = "4.6.6"
clap_complete_fig = "4.5.2" clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0" clap_complete_nushell = "4.6.0"

View file

@ -42,7 +42,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
# External build dependencies # External build dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
clap_complete = "4.6.5" clap_complete = "4.6.6"
clap_complete_fig = "4.5.2" clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0" clap_complete_nushell = "4.6.0"
serde = { workspace = true } serde = { workspace = true }

View file

@ -10,6 +10,7 @@ inventory::submit! {
// TODO: use a cache // TODO: use a cache
Ok(THEME.icon.matches(me, false)) Ok(THEME.icon.matches(me, false))
}); });
} },
from_lua: |_| Err(mlua::Error::UserDataTypeMismatch),
} }
} }

View file

@ -1,10 +1,13 @@
use ratatui_core::text::{Line, Text}; use std::slice;
use ratatui_core::text::{Line, Span, Text};
use ratatui_widgets::paragraph::{Paragraph, Wrap}; use ratatui_widgets::paragraph::{Paragraph, Wrap};
use yazi_binding::position::Position; use yazi_binding::position::Position;
use yazi_fs::file::File;
use yazi_macro::impl_data_any; use yazi_macro::impl_data_any;
use yazi_shared::{strand::ToStrand, url::UrlBuf}; use yazi_shared::strand::ToStrand;
use crate::YAZI; use crate::{THEME, YAZI};
// --- PickCfg // --- PickCfg
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -42,30 +45,30 @@ impl ConfirmCfg {
} }
} }
pub fn trash(urls: &[yazi_shared::url::UrlBuf]) -> Self { pub fn trash(files: &[File]) -> Self {
Self::new( Self::new(
Self::replace_number(&YAZI.confirm.trash_title, urls.len()), Self::replace_number(&YAZI.confirm.trash_title, files.len()),
YAZI.confirm.trash_position(), YAZI.confirm.trash_position(),
None, None,
Self::truncate_list(urls, urls.len(), 100), Self::truncate_files(files, 100),
) )
} }
pub fn delete(urls: &[yazi_shared::url::UrlBuf]) -> Self { pub fn delete(files: &[File]) -> Self {
Self::new( Self::new(
Self::replace_number(&YAZI.confirm.delete_title, urls.len()), Self::replace_number(&YAZI.confirm.delete_title, files.len()),
YAZI.confirm.delete_position(), YAZI.confirm.delete_position(),
None, None,
Self::truncate_list(urls, urls.len(), 100), Self::truncate_files(files, 100),
) )
} }
pub fn overwrite(url: &UrlBuf) -> Self { pub fn overwrite(file: &File) -> Self {
Self::new( Self::new(
YAZI.confirm.overwrite_title.clone(), YAZI.confirm.overwrite_title.clone(),
YAZI.confirm.overwrite_position(), YAZI.confirm.overwrite_position(),
Some(Text::raw(&YAZI.confirm.overwrite_body)), Some(Text::raw(&YAZI.confirm.overwrite_body)),
Some(url.to_strand().into_string_lossy().into()), Self::truncate_files(slice::from_ref(file), 1),
) )
} }
@ -74,7 +77,7 @@ impl ConfirmCfg {
Self::replace_number(&YAZI.confirm.quit_title, len), Self::replace_number(&YAZI.confirm.quit_title, len),
YAZI.confirm.quit_position(), YAZI.confirm.quit_position(),
Some(Text::raw(&YAZI.confirm.quit_body)), Some(Text::raw(&YAZI.confirm.quit_body)),
Self::truncate_list(names, len, 10), Self::truncate_lines(names, len, 10),
) )
} }
@ -82,7 +85,7 @@ impl ConfirmCfg {
tpl.replace("{n}", &n.to_string()).replace("{s}", if n > 1 { "s" } else { "" }) tpl.replace("{n}", &n.to_string()).replace("{s}", if n > 1 { "s" } else { "" })
} }
fn truncate_list<I>(it: I, len: usize, max: usize) -> Option<Text<'static>> fn truncate_lines<I>(it: I, len: usize, max: usize) -> Option<Text<'static>>
where where
I: IntoIterator, I: IntoIterator,
I::Item: ToStrand, I::Item: ToStrand,
@ -97,4 +100,22 @@ impl ConfirmCfg {
} }
Some(Text::from_iter(lines)) Some(Text::from_iter(lines))
} }
fn truncate_files(files: &[File], max: usize) -> Option<Text<'static>> {
let mut lines = Vec::with_capacity(files.len().min(max + 1));
for (i, f) in files.iter().enumerate() {
if i >= max {
lines.push(Line::raw(format!("... and {} more", files.len() - max)));
break;
}
lines.push(Line::default());
if let Some(icon) = THEME.icon.matches(f, false) {
lines[i].push_span(Span::styled(icon.text, icon.style));
lines[i].push_span(" ");
}
lines[i].push_span(f.url.to_strand().into_string_lossy());
}
Some(lines.into())
}
} }

View file

@ -67,7 +67,7 @@ impl Splatable for Mgr {
tab tab
.checked_sub(1) .checked_sub(1)
.and_then(|tab| self.tabs.get(tab)) .and_then(|tab| self.tabs.get(tab))
.map(|tab| tab.selected_or_hovered()) .map(|tab| tab.selected_or_hovered_urls())
.unwrap_or_else(|| Box::new(iter::empty())) .unwrap_or_else(|| Box::new(iter::empty()))
.skip(idx.unwrap_or(0)) .skip(idx.unwrap_or(0))
.take(if idx.is_some() { 1 } else { usize::MAX }) .take(if idx.is_some() { 1 } else { usize::MAX })
@ -82,5 +82,5 @@ impl Splatable for Mgr {
.map(|h| h.url.as_url()) .map(|h| h.url.as_url())
} }
fn yanked(&self) -> impl Iterator<Item = Url<'_>> { self.yanked.iter().map(|u| u.as_url()) } fn yanked(&self) -> impl Iterator<Item = Url<'_>> { self.yanked.iter().map(|f| f.url.as_url()) }
} }

View file

@ -1,32 +1,34 @@
use std::ops::Deref; use std::ops::Deref;
use hashbrown::HashSet; use hashbrown::HashSet;
use indexmap::IndexSet; use indexmap::{IndexSet, set::MutableValues};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::FilesOp; use yazi_fs::{FilesOp, file::FileCov};
use yazi_macro::err; use yazi_macro::err;
use yazi_shared::url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlLike}; use yazi_shared::url::{Url, UrlBuf, UrlCov, UrlLike};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Yanked { pub struct Yanked {
pub cut: bool, pub cut: bool,
urls: IndexSet<UrlBufCov>, files: IndexSet<FileCov>,
version: u64, version: u64,
revision: u64, revision: u64,
} }
impl Deref for Yanked { impl Deref for Yanked {
type Target = IndexSet<UrlBufCov>; type Target = IndexSet<FileCov>;
fn deref(&self) -> &Self::Target { &self.urls } fn deref(&self) -> &Self::Target { &self.files }
} }
impl Yanked { impl Yanked {
pub fn new(cut: bool, urls: IndexSet<UrlBufCov>) -> Self { pub fn new(cut: bool, files: IndexSet<FileCov>) -> Self {
Self { cut, urls, ..Default::default() } Self { cut, files, ..Default::default() }
} }
pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { self.files.iter().map(|f| &f.url) }
pub fn remove_many<'a, I, T>(&mut self, urls: I) pub fn remove_many<'a, I, T>(&mut self, urls: I)
where where
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
@ -37,45 +39,47 @@ impl Yanked {
return; return;
} }
let old = self.urls.len(); let old = self.files.len();
self.urls.retain(|u| !urls.contains(&UrlCov::new(u))); self.files.retain(|f| !urls.contains(&UrlCov::new(&f.url)));
self.revision += (old != self.urls.len()) as u64; self.revision += (old != self.files.len()) as u64;
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
if self.urls.is_empty() { if self.files.is_empty() {
return; return;
} }
self.urls.clear(); self.files.clear();
self.revision += 1; self.revision += 1;
} }
pub fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool { pub fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool {
self.urls.contains(&UrlCov::new(url)) self.files.contains(&UrlCov::new(url))
} }
pub fn contains_in(&self, dir: &UrlBuf) -> bool { pub fn contains_in(&self, dir: &UrlBuf) -> bool {
self.urls.iter().any(|u| { self.files.iter().any(|f| {
let mut it = u.components(); let mut it = f.url.components();
it.next_back().is_some() it.next_back().is_some()
&& it.covariant(&dir.components()) && it.covariant(&dir.components())
&& u.parent().is_some_and(|p| p == *dir) && f.url.parent().is_some_and(|p| p == *dir)
}) })
} }
pub fn apply_op(&mut self, op: &FilesOp) { pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); let (removal, addition) = op.diff_recoverable(|u| self.contains(u));
if !removal.is_empty() { if !removal.is_empty() {
let old = self.urls.len(); let old = self.files.len();
self.urls.retain(|u| !removal.contains(u)); self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u)));
self.revision += (old != self.urls.len()) as u64; self.revision += (old != self.files.len()) as u64;
} }
if !addition.is_empty() { if !addition.is_empty() {
let old = self.urls.len(); let old = self.files.len();
self.urls.extend(addition.into_iter().map(UrlBufCov)); self.files.extend(addition.into_iter().map(FileCov));
self.revision += (old != self.urls.len()) as u64; self.revision += (old != self.files.len()) as u64;
}
for f in op.files() {
self.files.get_full_mut2(&UrlCov::new(&f.url)).map(|(_, v)| *v = f.into());
} }
} }
@ -85,7 +89,7 @@ impl Yanked {
} }
self.version = self.revision; self.version = self.revision;
err!(Pubsub::pub_after_yank(self.cut, &self.urls)); err!(Pubsub::pub_after_yank(self.cut, &self.files));
true true
} }
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use hashbrown::HashMap; use hashbrown::HashMap;
use yazi_fs::{Files, Filter, FilterCase}; use yazi_fs::{Entries, Filter, FilterCase};
use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf};
use crate::tab::Folder; use crate::tab::Folder;
@ -26,10 +26,10 @@ impl Finder {
}) })
} }
pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> { pub fn prev(&self, entries: &Entries, cursor: usize, include: bool) -> Option<isize> {
for i in !include as usize..files.len() { for i in !include as usize..entries.len() {
let idx = (cursor + files.len() - i) % files.len(); let idx = (cursor + entries.len() - i) % entries.len();
if let Some(s) = files[idx].name() if let Some(s) = entries[idx].name()
&& self.filter.matches(s) && self.filter.matches(s)
{ {
return Some(idx as isize - cursor as isize); return Some(idx as isize - cursor as isize);
@ -38,10 +38,10 @@ impl Finder {
None None
} }
pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> { pub fn next(&self, entries: &Entries, cursor: usize, include: bool) -> Option<isize> {
for i in !include as usize..files.len() { for i in !include as usize..entries.len() {
let idx = (cursor + i) % files.len(); let idx = (cursor + i) % entries.len();
if let Some(s) = files[idx].name() if let Some(s) = entries[idx].name()
&& self.filter.matches(s) && self.filter.matches(s)
{ {
return Some(idx as isize - cursor as isize); return Some(idx as isize - cursor as isize);
@ -57,7 +57,7 @@ impl Finder {
self.matched.clear(); self.matched.clear();
let mut i = 0u8; let mut i = 0u8;
for file in folder.files.iter() { for file in folder.entries.iter() {
if file.name().is_none_or(|s| !self.filter.matches(s)) { if file.name().is_none_or(|s| !self.filter.matches(s)) {
continue; continue;
} }
@ -87,12 +87,12 @@ impl Finder {
// --- Lock // --- Lock
impl From<&Folder> for FinderLock { impl From<&Folder> for FinderLock {
fn from(value: &Folder) -> Self { fn from(value: &Folder) -> Self {
Self { cwd: value.url.clone(), revision: value.files.revision } Self { cwd: value.url.clone(), revision: value.entries.revision }
} }
} }
impl PartialEq<Folder> for FinderLock { impl PartialEq<Folder> for FinderLock {
fn eq(&self, other: &Folder) -> bool { fn eq(&self, other: &Folder) -> bool {
self.revision == other.files.revision && self.cwd == other.url self.revision == other.entries.revision && self.cwd == other.url
} }
} }

View file

@ -2,7 +2,7 @@ use std::mem;
use yazi_config::{LAYOUT, YAZI}; use yazi_config::{LAYOUT, YAZI};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{Files, FilesOp, FolderStage, cha::Cha, file::File}; use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File};
use yazi_macro::err; use yazi_macro::err;
use yazi_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf};
use yazi_widgets::{Scrollable, Step}; use yazi_widgets::{Scrollable, Step};
@ -10,10 +10,10 @@ use yazi_widgets::{Scrollable, Step};
use crate::MgrProxy; use crate::MgrProxy;
pub struct Folder { pub struct Folder {
pub url: UrlBuf, pub url: UrlBuf,
pub cha: Cha, pub cha: Cha,
pub files: Files, pub entries: Entries,
pub stage: FolderStage, pub stage: FolderStage,
pub offset: usize, pub offset: usize,
pub cursor: usize, pub cursor: usize,
@ -25,14 +25,14 @@ pub struct Folder {
impl Default for Folder { impl Default for Folder {
fn default() -> Self { fn default() -> Self {
Self { Self {
url: Default::default(), url: Default::default(),
cha: Default::default(), cha: Default::default(),
files: Files::new(YAZI.mgr.show_hidden.get()), entries: Entries::new(YAZI.mgr.show_hidden.get()),
stage: Default::default(), stage: Default::default(),
offset: Default::default(), offset: Default::default(),
cursor: Default::default(), cursor: Default::default(),
page: Default::default(), page: Default::default(),
trace: Default::default(), trace: Default::default(),
} }
} }
} }
@ -43,7 +43,7 @@ impl<T: Into<UrlBuf>> From<T> for Folder {
impl Folder { impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool { pub fn update(&mut self, op: FilesOp) -> bool {
let (stage, revision) = (self.stage.clone(), self.files.revision); let (stage, revision) = (self.stage.clone(), self.entries.revision);
match op { match op {
FilesOp::Full(_, _, cha) => { FilesOp::Full(_, _, cha) => {
(self.cha, self.stage) = (cha, FolderStage::Loaded); (self.cha, self.stage) = (cha, FolderStage::Loaded);
@ -51,10 +51,10 @@ impl Folder {
FilesOp::Part(_, ref files, _) if files.is_empty() => { FilesOp::Part(_, ref files, _) if files.is_empty() => {
(self.cha, self.stage) = (Cha::default(), FolderStage::Loading); (self.cha, self.stage) = (Cha::default(), FolderStage::Loading);
} }
FilesOp::Part(_, _, ticket) if ticket == self.files.ticket() => { FilesOp::Part(_, _, ticket) if ticket == self.entries.ticket() => {
self.stage = FolderStage::Loading; self.stage = FolderStage::Loading;
} }
FilesOp::Done(_, cha, ticket) if ticket == self.files.ticket() => { FilesOp::Done(_, cha, ticket) if ticket == self.entries.ticket() => {
(self.cha, self.stage) = (cha, FolderStage::Loaded); (self.cha, self.stage) = (cha, FolderStage::Loaded);
} }
FilesOp::IOErr(_, ref err) => { FilesOp::IOErr(_, ref err) => {
@ -65,23 +65,23 @@ impl Folder {
let mut deleted = vec![]; let mut deleted = vec![];
match op { match op {
FilesOp::Full(_, files, _) => self.files.update_full(files), FilesOp::Full(_, files, _) => self.entries.update_full(files),
FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket), FilesOp::Part(_, files, ticket) => self.entries.update_part(files, ticket),
FilesOp::Done(..) => {} FilesOp::Done(..) => {}
FilesOp::Size(_, sizes) => self.files.update_size(sizes), FilesOp::Size(_, sizes) => self.entries.update_size(sizes),
FilesOp::IOErr(..) => self.files.update_ioerr(), FilesOp::IOErr(..) => self.entries.update_ioerr(),
FilesOp::Creating(_, files) => self.files.update_creating(files), FilesOp::Creating(_, files) => self.entries.update_creating(files),
FilesOp::Deleting(_, urns) => deleted = self.files.update_deleting(urns), FilesOp::Deleting(_, urns) => deleted = self.entries.update_deleting(urns),
FilesOp::Updating(_, files) => _ = self.files.update_updating(files), FilesOp::Updating(_, files) => _ = self.entries.update_updating(files),
FilesOp::Upserting(_, files) => self.files.update_upserting(files), FilesOp::Upserting(_, files) => self.entries.update_upserting(files),
}; };
self.trace.take_if(|_| self.files.is_empty() && !self.stage.is_loading()); self.trace.take_if(|_| self.entries.is_empty() && !self.stage.is_loading());
self.arrow(-(deleted.into_iter().filter(|&i| i < self.cursor).count() as isize)); self.arrow(-(deleted.into_iter().filter(|&i| i < self.cursor).count() as isize));
self.repos(None); self.repos(None);
(&stage, revision) != (&self.stage, self.files.revision) (&stage, revision) != (&self.stage, self.entries.revision)
} }
pub fn update_pub(&mut self, tab: Id, op: FilesOp) -> bool { pub fn update_pub(&mut self, tab: Id, op: FilesOp) -> bool {
@ -93,7 +93,7 @@ impl Folder {
} }
pub fn arrow(&mut self, step: impl Into<Step>) -> bool { pub fn arrow(&mut self, step: impl Into<Step>) -> bool {
let mut b = if self.files.is_empty() { let mut b = if self.entries.is_empty() {
(mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0) (mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0)
} else { } else {
self.scroll(step) self.scroll(step)
@ -109,7 +109,7 @@ impl Folder {
return self.arrow(0); return self.arrow(0);
} }
let new = self.files.position(urn).unwrap_or(self.cursor) as isize; let new = self.entries.position(urn).unwrap_or(self.cursor) as isize;
let b = self.arrow(new - self.cursor as isize); let b = self.arrow(new - self.cursor as isize);
self.retrace(); self.retrace();
@ -146,7 +146,7 @@ impl Folder {
fn squeeze_offset(&mut self) -> bool { fn squeeze_offset(&mut self) -> bool {
let old = self.offset; let old = self.offset;
let len = self.files.len(); let len = self.entries.len();
let limit = LAYOUT.get().folder_limit(); let limit = LAYOUT.get().folder_limit();
let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff.get() as usize); let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff.get() as usize);
@ -164,23 +164,23 @@ impl Folder {
impl Folder { impl Folder {
#[inline] #[inline]
pub fn hovered(&self) -> Option<&File> { self.files.get(self.cursor) } pub fn hovered(&self) -> Option<&File> { self.entries.get(self.cursor) }
#[inline] #[inline]
pub fn hovered_mut(&mut self) -> Option<&mut File> { self.files.get_mut(self.cursor) } pub fn hovered_mut(&mut self) -> Option<&mut File> { self.entries.get_mut(self.cursor) }
pub fn paginate(&self, page: usize) -> &[File] { pub fn paginate(&self, page: usize) -> &[File] {
let len = self.files.len(); let len = self.entries.len();
let limit = LAYOUT.get().folder_limit(); let limit = LAYOUT.get().folder_limit();
let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1)); let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1));
let end = ((page + 2) * limit).min(len); let end = ((page + 2) * limit).min(len);
&self.files[start..end] &self.entries[start..end]
} }
} }
impl Scrollable for Folder { impl Scrollable for Folder {
fn total(&self) -> usize { self.files.len() } fn total(&self) -> usize { self.entries.len() }
fn limit(&self) -> usize { LAYOUT.get().folder_limit() } fn limit(&self) -> usize { LAYOUT.get().folder_limit() }

View file

@ -4,11 +4,11 @@ use tokio::{pin, task::JoinHandle};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_adapter::ADAPTOR; use yazi_adapter::ADAPTOR;
use yazi_config::{LAYOUT, YAZI}; use yazi_config::{LAYOUT, YAZI};
use yazi_fs::{Files, FilesOp, cha::Cha, file::File}; use yazi_fs::{Entries, FilesOp, cha::Cha, file::File};
use yazi_macro::render; use yazi_macro::render;
use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}}; use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}};
use yazi_shared::{pool::Symbol, url::{UrlBuf, UrlLike}}; use yazi_shared::{pool::Symbol, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFiles, VfsFilesOp}; use yazi_vfs::{VfsEntries, VfsFilesOp};
use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock}; use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock};
@ -60,9 +60,9 @@ impl Preview {
self.folder_lock = Some(wd.clone()); self.folder_lock = Some(wd.clone());
self.folder_loader.take().map(|h| h.abort()); self.folder_loader.take().map(|h| h.abort());
self.folder_loader = Some(tokio::spawn(async move { self.folder_loader = Some(tokio::spawn(async move {
let Some(new) = Files::assert_stale(&wd, dir.unwrap_or_default()).await else { return }; let Some(new) = Entries::assert_stale(&wd, dir.unwrap_or_default()).await else { return };
let rx = match Files::from_dir(&wd).await { let rx = match Entries::from_dir(&wd).await {
Ok(rx) => rx, Ok(rx) => rx,
Err(e) => return FilesOp::issue_error(&wd, e).await, Err(e) => return FilesOp::issue_error(&wd, e).await,
}; };

View file

@ -1,13 +1,11 @@
use std::ops::Deref;
use hashbrown::HashMap; use hashbrown::HashMap;
use indexmap::IndexMap; use indexmap::{IndexMap, map::MutableKeys};
use yazi_fs::FilesOp; use yazi_fs::{FilesOp, file::{File, FileCov}};
use yazi_shared::{timestamp_us, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCovMapExt}}; use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlBuf, UrlBufCov, UrlCov, UrlCovMapExt, UrlLike, UrlMapExt}};
#[derive(Default)] #[derive(Default)]
pub struct Selected { pub struct Selected {
inner: IndexMap<UrlBufCov, u64>, inner: IndexMap<FileCov, u64>,
parents: HashMap<UrlBufCov, usize>, parents: HashMap<UrlBufCov, usize>,
} }
@ -16,64 +14,70 @@ impl Selected {
pub fn is_empty(&self) -> bool { self.inner.is_empty() } pub fn is_empty(&self) -> bool { self.inner.is_empty() }
pub fn values(&self) -> impl Iterator<Item = &UrlBuf> { self.inner.keys().map(Deref::deref) } pub fn files(&self) -> impl Iterator<Item = &File> { self.inner.keys().map(|f| &f.0) }
pub fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool { pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { self.inner.keys().map(|f| &f.url) }
self.inner.contains_key(&UrlCov::new(url))
pub fn contains(&self, url: impl AsUrl) -> bool {
self.inner.contains_key(&UrlCov::new(url.as_url()))
} }
pub fn add<'a>(&mut self, url: impl Into<Url<'a>>) -> bool { self.add_same([url]) == 1 } pub fn add(&mut self, file: impl AsUrl + Into<File>) -> bool { self.add_same([file]) == 1 }
pub fn add_many<'a, I, T>(&mut self, urls: I) -> usize pub fn add_many<I, T>(&mut self, files: I) -> usize
where where
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
T: Into<Url<'a>>, T: AsUrl + Into<File>,
{ {
let mut grouped: IndexMap<_, Vec<_>> = Default::default(); let mut grouped: IndexMap<_, Vec<_>> = Default::default();
for url in urls.into_iter().map(Into::into) { for file in files {
if let Some(p) = url.parent() { if let Some(p) = file.as_url().parent() {
grouped.entry(p).or_default().push(url); grouped.get_or_insert_default(p).push(file);
} }
} }
grouped.into_values().map(|v| self.add_same(v)).sum() grouped.into_values().map(|v| self.add_same(v)).sum()
} }
fn add_same<'a, I, T>(&mut self, urls: I) -> usize fn add_same<I, T>(&mut self, files: I) -> usize
where where
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
T: Into<Url<'a>>, T: AsUrl + Into<File>,
{ {
// If it has appeared as a parent // If it has appeared as a parent
let urls: Vec<_> = let files: Vec<_> =
urls.into_iter().map(UrlCov::new).filter(|u| !self.parents.contains_key(u)).collect(); files.into_iter().filter(|f| !self.parents.contains_key(&UrlCov::new(f.as_url()))).collect();
if urls.is_empty() { if files.is_empty() {
return 0; return 0;
} }
// If it has appeared as a child // If it has appeared as a child
let mut parent = urls[0].parent(); let mut parent = files[0].as_url().parent();
let mut parents = vec![];
while let Some(u) = parent { while let Some(u) = parent {
if self.inner.contains_key(&UrlCov::new(u)) { if self.inner.contains_key(&UrlCov::new(u)) {
return 0; return 0;
} }
parent = u.parent(); parent = u.parent();
parents.push(u);
} }
let (now, len) = (timestamp_us(), self.inner.len()); // Mark the files as selected children with a timestamp
self.inner.extend(urls.iter().enumerate().map(|(i, u)| (u.into(), now + i as u64))); let (now, fl, il) = (timestamp_us(), files.len(), self.inner.len());
self
.inner
.extend(files.into_iter().enumerate().map(|(i, f)| (FileCov(f.into()), now + i as u64)));
for u in parents { // Update the parent counts
*self.parents.get_or_insert_default(UrlCov::new(u)) += self.inner.len() - len; if let Some((first, _)) = self.inner.get_index(il) {
let mut parent = first.url.parent();
while let Some(u) = parent {
*self.parents.get_or_insert_default(UrlCov::new(u)) += self.inner.len() - il;
parent = u.parent();
}
} }
urls.len()
fl
} }
pub fn remove<'a>(&mut self, url: impl Into<Url<'a>> + Clone) -> bool { pub fn remove(&mut self, url: impl AsUrl) -> bool { self.remove_same([url]) == 1 }
self.remove_same([url]) == 1
}
pub fn remove_many<'a, I, T>(&mut self, urls: I) -> usize pub fn remove_many<'a, I, T>(&mut self, urls: I) -> usize
where where
@ -95,21 +99,21 @@ impl Selected {
affected affected
} }
fn remove_same<'a, I, T>(&mut self, urls: I) -> usize fn remove_same<I, T>(&mut self, urls: I) -> usize
where where
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
T: Into<Url<'a>> + Clone, T: AsUrl,
{ {
let mut it = urls.into_iter().peekable(); let mut it = urls.into_iter();
let Some(first) = it.peek().cloned().map(UrlCov::new) else { return 0 }; let Some(first) = it.next() else { return 0 };
let count = it.filter_map(|u| self.inner.swap_remove(&UrlCov::new(u))).count(); let count = self.inner.swap_remove(&UrlCov::new(first.as_url())).is_some() as usize
+ it.filter_map(|u| self.inner.swap_remove(&UrlCov::new(u.as_url()))).count();
if count == 0 { if count == 0 {
return 0; return 0;
} }
// FIXME: use UrlCov::parent() instead let mut parent = first.as_url().parent();
let mut parent = first.parent();
while let Some(u) = parent { while let Some(u) = parent {
let n = self.parents.get_mut(&UrlCov::new(u)).unwrap(); let n = self.parents.get_mut(&UrlCov::new(u)).unwrap();
@ -134,23 +138,28 @@ impl Selected {
self.remove_many(&removal); self.remove_many(&removal);
} }
if !addition.is_empty() { if !addition.is_empty() {
self.add_many(&addition); self.add_many(addition);
}
for f in op.files() {
self.inner.get_full_mut2(&UrlCov::new(&f.url)).map(|(_, k, _)| *k = f.into());
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path; use std::{ffi::OsStr, path::Path};
use super::*; use super::*;
fn f<S: AsRef<OsStr> + ?Sized>(s: &S) -> File { File::from_dummy(Path::new(s), None) }
#[test] #[test]
fn test_insert_non_conflicting() { fn test_insert_non_conflicting() {
let mut s = Selected::default(); let mut s = Selected::default();
assert!(s.add(Path::new("/a/b"))); assert!(s.add(f("/a/b")));
assert!(s.add(Path::new("/c/d"))); assert!(s.add(f("/c/d")));
assert_eq!(s.inner.len(), 2); assert_eq!(s.inner.len(), 2);
} }
@ -158,24 +167,24 @@ mod tests {
fn test_insert_conflicting_parent() { fn test_insert_conflicting_parent() {
let mut s = Selected::default(); let mut s = Selected::default();
assert!(s.add(Path::new("/a"))); assert!(s.add(f("/a")));
assert!(!s.add(Path::new("/a/b"))); assert!(!s.add(f("/a/b")));
} }
#[test] #[test]
fn test_insert_conflicting_child() { fn test_insert_conflicting_child() {
let mut s = Selected::default(); let mut s = Selected::default();
assert!(s.add(Path::new("/a/b/c"))); assert!(s.add(f("/a/b/c")));
assert!(!s.add(Path::new("/a/b"))); assert!(!s.add(f("/a/b")));
assert!(s.add(Path::new("/a/b/d"))); assert!(s.add(f("/a/b/d")));
} }
#[test] #[test]
fn test_remove() { fn test_remove() {
let mut s = Selected::default(); let mut s = Selected::default();
assert!(s.add(Path::new("/a/b"))); assert!(s.add(f("/a/b")));
assert!(!s.remove(Path::new("/a/c"))); assert!(!s.remove(Path::new("/a/c")));
assert!(s.remove(Path::new("/a/b"))); assert!(s.remove(Path::new("/a/b")));
assert!(!s.remove(Path::new("/a/b"))); assert!(!s.remove(Path::new("/a/b")));
@ -184,92 +193,83 @@ mod tests {
} }
#[test] #[test]
fn insert_many_success() { fn add_many_success() {
let mut s = Selected::default(); let mut s = Selected::default();
assert_eq!( assert_eq!(3, s.add_same([f("/parent/child1"), f("/parent/child2"), f("/parent/child3")]));
3,
s.add_same([
Path::new("/parent/child1"),
Path::new("/parent/child2"),
Path::new("/parent/child3")
])
);
} }
#[test] #[test]
fn insert_many_with_existing_parent_fails() { fn add_many_with_existing_parent_fails() {
let mut s = Selected::default(); let mut s = Selected::default();
s.add(Path::new("/parent")); s.add(f("/parent"));
assert_eq!(0, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); assert_eq!(0, s.add_same([f("/parent/child1"), f("/parent/child2")]));
} }
#[test] #[test]
fn insert_many_with_existing_child_fails() { fn add_many_with_existing_child_fails() {
let mut s = Selected::default(); let mut s = Selected::default();
s.add(Path::new("/parent/child1")); s.add(f("/parent/child1"));
assert_eq!(2, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); assert_eq!(2, s.add_same([f("/parent/child1"), f("/parent/child2")]));
} }
#[test] #[test]
fn insert_many_empty_urls_list() { fn add_many_empty_files_list() {
let mut s = Selected::default(); let mut s = Selected::default();
assert_eq!(0, s.add_same([] as [Url; 0])); assert_eq!(0, s.add_same([] as [File; 0]));
} }
#[test] #[test]
fn insert_many_with_parent_as_child_of_another_url() { fn add_many_with_parent_as_child_of_another_url() {
let mut s = Selected::default(); let mut s = Selected::default();
s.add(Path::new("/parent/child")); s.add(f("/parent/child"));
assert_eq!( assert_eq!(0, s.add_same([f("/parent/child/child1"), f("/parent/child/child2")]));
0,
s.add_same([Path::new("/parent/child/child1"), Path::new("/parent/child/child2")])
);
}
#[test]
fn insert_many_with_direct_parent_fails() {
let mut s = Selected::default();
s.add(Path::new("/a"));
assert_eq!(0, s.add_same([Path::new("/a/b")]));
} }
#[test] #[test]
fn insert_many_with_nested_child_fails() { fn add_many_with_direct_parent_fails() {
let mut s = Selected::default(); let mut s = Selected::default();
s.add(Path::new("/a/b")); s.add(f("/a"));
assert_eq!(0, s.add_same([Path::new("/a")])); assert_eq!(0, s.add_same([f("/a/b")]));
assert_eq!(1, s.add_same([Path::new("/b"), Path::new("/a")]));
} }
#[test] #[test]
fn insert_many_sibling_directories_success() { fn add_many_with_nested_child_fails() {
let mut s = Selected::default(); let mut s = Selected::default();
assert_eq!(2, s.add_same([Path::new("/a/b"), Path::new("/a/c")])); s.add(f("/a/b"));
assert_eq!(0, s.add_same([f("/a")]));
assert_eq!(1, s.add_same([f("/b"), f("/a")]));
} }
#[test] #[test]
fn insert_many_with_grandchild_fails() { fn add_many_sibling_directories_success() {
let mut s = Selected::default(); let mut s = Selected::default();
s.add(Path::new("/a/b")); assert_eq!(2, s.add_same([f("/a/b"), f("/a/c")]));
assert_eq!(0, s.add_same([Path::new("/a/b/c")]));
} }
#[test] #[test]
fn test_insert_many_with_remove() { fn add_many_with_grandchild_fails() {
let mut s = Selected::default();
s.add(f("/a/b"));
assert_eq!(0, s.add_same([f("/a/b/c")]));
}
#[test]
fn test_add_many_with_remove() {
let mut s = Selected::default(); let mut s = Selected::default();
let child1 = Path::new("/parent/child1"); let child1 = Path::new("/parent/child1");
let child2 = Path::new("/parent/child2"); let child2 = Path::new("/parent/child2");
let child3 = Path::new("/parent/child3"); let child3 = Path::new("/parent/child3");
assert_eq!(3, s.add_same([child1, child2, child3])); assert_eq!(3, s.add_same([f(child1), f(child2), f(child3)]));
assert!(s.remove(child1)); assert!(s.remove(child1));
assert_eq!(s.inner.len(), 2); assert_eq!(s.inner.len(), 2);
@ -282,4 +282,13 @@ mod tests {
assert!(s.inner.is_empty()); assert!(s.inner.is_empty());
assert!(s.parents.is_empty()); assert!(s.parents.is_empty());
} }
#[test]
fn add_same_all_duplicates() {
let mut s = Selected::default();
s.add(f("/a/b"));
assert_eq!(1, s.add_same([f("/a/b")]));
assert_eq!(s.inner.len(), 1);
}
} }

View file

@ -82,7 +82,7 @@ impl Tab {
pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() }
pub fn hovered_rect(&self) -> Option<Rect> { pub fn hovered_rect(&self) -> Option<Rect> {
let y = self.current.files.position(self.hovered()?.urn())? - self.current.offset; let y = self.current.entries.position(self.hovered()?.urn())? - self.current.offset;
let mut rect = LAYOUT.get().current; let mut rect = LAYOUT.get().current;
rect.y = rect.y.saturating_sub(1) + y as u16; rect.y = rect.y.saturating_sub(1) + y as u16;
@ -99,22 +99,30 @@ impl Tab {
} }
} }
pub fn selected_or_hovered(&self) -> Box<dyn Iterator<Item = &UrlBuf> + '_> { pub fn selected_or_hovered_files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
if self.selected.is_empty() {
Box::new(self.hovered().into_iter())
} else {
Box::new(self.selected.files())
}
}
pub fn selected_or_hovered_urls(&self) -> Box<dyn Iterator<Item = &UrlBuf> + '_> {
if self.selected.is_empty() { if self.selected.is_empty() {
Box::new(self.hovered().map(|h| &h.url).into_iter()) Box::new(self.hovered().map(|h| &h.url).into_iter())
} else { } else {
Box::new(self.selected.values()) Box::new(self.selected.urls())
} }
} }
pub fn hovered_and_selected(&self) -> Box<dyn Iterator<Item = &UrlBuf> + '_> { pub fn hovered_and_selected(&self) -> Box<dyn Iterator<Item = &UrlBuf> + '_> {
let Some(h) = self.hovered() else { let Some(h) = self.hovered() else {
return Box::new([UrlBuf::new()].into_iter().chain(self.selected.values())); return Box::new([UrlBuf::new()].into_iter().chain(self.selected.urls()));
}; };
if self.selected.is_empty() { if self.selected.is_empty() {
Box::new([&h.url, &h.url].into_iter()) Box::new([&h.url, &h.url].into_iter())
} else { } else {
Box::new([&h.url].into_iter().chain(self.selected.values())) Box::new([&h.url].into_iter().chain(self.selected.urls()))
} }
} }

View file

@ -1,7 +1,6 @@
use indexmap::IndexSet;
use tracing::debug; use tracing::debug;
use yazi_scheduler::file::FileInCut; use yazi_scheduler::file::FileInCut;
use yazi_shared::url::{UrlBuf, UrlBufCov, UrlLike}; use yazi_shared::url::{UrlBuf, UrlLike};
use super::Tasks; use super::Tasks;
use crate::mgr::Yanked; use crate::mgr::Yanked;
@ -10,15 +9,15 @@ impl Tasks {
pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) {
self.scheduler.behavior.reset(); self.scheduler.behavior.reset();
for u in src.iter() { for u in src.urls() {
let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else {
debug!("file_cut: cannot join {u:?} with {dest:?}"); debug!("file_cut: cannot join {u:?} with {dest:?}");
continue; continue;
}; };
if force && *u == to { if force && u == to {
debug!("file_cut: same file, skip {to:?}"); debug!("file_cut: same file, skip {to:?}");
} else { } else {
self.scheduler.file_cut(FileInCut::new(u.0.clone(), to, force)); self.scheduler.file_cut(FileInCut::new(u.clone(), to, force));
} }
} }
} }
@ -26,47 +25,47 @@ impl Tasks {
pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) {
self.scheduler.behavior.reset(); self.scheduler.behavior.reset();
for u in src.iter() { for u in src.urls() {
let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else {
debug!("file_copy: cannot join {u:?} with {dest:?}"); debug!("file_copy: cannot join {u:?} with {dest:?}");
continue; continue;
}; };
if force && *u == to { if force && u == to {
debug!("file_copy: same file, skip {to:?}"); debug!("file_copy: same file, skip {to:?}");
} else { } else {
self.scheduler.file_copy(u.0.clone(), to, force, follow); self.scheduler.file_copy(u.clone(), to, force, follow);
} }
} }
} }
pub fn file_link(&self, src: &IndexSet<UrlBufCov>, dest: &UrlBuf, relative: bool, force: bool) { pub fn file_link(&self, src: &Yanked, dest: &UrlBuf, relative: bool, force: bool) {
self.scheduler.behavior.reset(); self.scheduler.behavior.reset();
for u in src { for u in src.urls() {
let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else {
debug!("file_link: cannot join {u:?} with {dest:?}"); debug!("file_link: cannot join {u:?} with {dest:?}");
continue; continue;
}; };
if force && *u == to { if force && u == to {
debug!("file_link: same file, skip {to:?}"); debug!("file_link: same file, skip {to:?}");
} else { } else {
self.scheduler.file_link(u.0.clone(), to, relative, force); self.scheduler.file_link(u.clone(), to, relative, force);
} }
} }
} }
pub fn file_hardlink(&self, src: &IndexSet<UrlBufCov>, dest: &UrlBuf, force: bool, follow: bool) { pub fn file_hardlink(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) {
self.scheduler.behavior.reset(); self.scheduler.behavior.reset();
for u in src { for u in src.urls() {
let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else {
debug!("file_hardlink: cannot join {u:?} with {dest:?}"); debug!("file_hardlink: cannot join {u:?} with {dest:?}");
continue; continue;
}; };
if force && *u == to { if force && u == to {
debug!("file_hardlink: same file, skip {to:?}"); debug!("file_hardlink: same file, skip {to:?}");
} else { } else {
self.scheduler.file_hardlink(u.0.clone(), to, force, follow); self.scheduler.file_hardlink(u.clone(), to, force, follow);
} }
} }
} }

View file

@ -1,5 +1,5 @@
use yazi_config::{YAZI, plugin::MAX_FETCHERS}; use yazi_config::{YAZI, plugin::MAX_FETCHERS};
use yazi_fs::{Files, FsHash64, SortBy, file::File}; use yazi_fs::{Entries, FsHash64, SortBy, file::File};
use super::Tasks; use super::Tasks;
use crate::mgr::Mimetype; use crate::mgr::Mimetype;
@ -44,7 +44,7 @@ impl Tasks {
} }
} }
pub fn prework_sorted(&self, targets: &Files) { pub fn prework_sorted(&self, targets: &Entries) {
if targets.sorter().by != SortBy::Size { if targets.sorter().by != SortBy::Size {
return; return;
} }

View file

@ -3,34 +3,31 @@ use std::borrow::Cow;
use indexmap::IndexSet; use indexmap::IndexSet;
use mlua::{AnyUserData, FromLua, IntoLua, Lua, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, FromLua, IntoLua, Lua, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yazi_fs::file::FileCov;
use yazi_macro::impl_data_any; use yazi_macro::impl_data_any;
use yazi_shared::url::{UrlBuf, UrlBufCov};
use yazi_shim::mlua::get_metatable; use yazi_shim::mlua::get_metatable;
use super::Ember; use super::Ember;
type Iter = yazi_binding::Iter< type Iter = yazi_binding::Iter<indexmap::set::IntoIter<FileCov>, FileCov>;
std::iter::Map<indexmap::set::IntoIter<UrlBufCov>, fn(UrlBufCov) -> UrlBuf>,
UrlBuf,
>;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmberYank<'a> { pub struct EmberYank<'a> {
pub cut: bool, pub cut: bool,
pub urls: Cow<'a, IndexSet<UrlBufCov>>, pub files: Cow<'a, IndexSet<FileCov>>,
} }
impl_data_any!(EmberYank<'static>, from_into_lua = inherit); impl_data_any!(EmberYank<'static>, from_into_lua = inherit);
impl<'a> EmberYank<'a> { impl<'a> EmberYank<'a> {
pub fn borrowed(cut: bool, urls: &'a IndexSet<UrlBufCov>) -> Ember<'a> { pub fn borrowed(cut: bool, files: &'a IndexSet<FileCov>) -> Ember<'a> {
Self { cut, urls: Cow::Borrowed(urls) }.into() Self { cut, files: Cow::Borrowed(files) }.into()
} }
} }
impl EmberYank<'static> { impl EmberYank<'static> {
pub fn owned(cut: bool, _: &IndexSet<UrlBufCov>) -> Ember<'static> { pub fn owned(cut: bool, _: &IndexSet<FileCov>) -> Ember<'static> {
Self { cut, urls: Default::default() }.into() Self { cut, files: Default::default() }.into()
} }
} }
@ -53,8 +50,8 @@ impl FromLua for EmberYank<'static> {
impl IntoLua for EmberYank<'_> { impl IntoLua for EmberYank<'_> {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
let len = self.urls.len(); let len = self.files.len();
let iter = Iter::new(self.urls.into_owned().into_iter().map(UrlBuf::from), Some(len)); let iter = Iter::new(self.files.into_owned().into_iter(), Some(len));
EmberYankIter { cut: self.cut, len, inner: lua.create_userdata(iter)? }.into_lua(lua) EmberYankIter { cut: self.cut, len, inner: lua.create_userdata(iter)? }.into_lua(lua)
} }
} }
@ -69,15 +66,8 @@ pub struct EmberYankIter {
impl EmberYankIter { impl EmberYankIter {
pub fn collect(self, lua: &Lua) -> mlua::Result<EmberYank<'static>> { pub fn collect(self, lua: &Lua) -> mlua::Result<EmberYank<'static>> {
Ok(EmberYank { Ok(EmberYank {
cut: self.cut, cut: self.cut,
urls: Cow::Owned( files: Cow::Owned(self.inner.take::<Iter>()?.into_iter(lua).collect::<mlua::Result<_>>()?),
self
.inner
.take::<Iter>()?
.into_iter(lua)
.map(|result| result.map(Into::into))
.collect::<mlua::Result<_>>()?,
),
}) })
} }
} }

View file

@ -4,8 +4,8 @@ use indexmap::IndexSet;
use mlua::Function; use mlua::Function;
use parking_lot::RwLock; use parking_lot::RwLock;
use yazi_boot::BOOT; use yazi_boot::BOOT;
use yazi_fs::FolderStage; use yazi_fs::{FolderStage, file::FileCov};
use yazi_shared::{id::Id, url::{Url, UrlBuf, UrlBufCov}}; use yazi_shared::{id::Id, url::{Url, UrlBuf}};
use yazi_shim::cell::RoCell; use yazi_shim::cell::RoCell;
use crate::{Client, ID, PEERS, ember::{Ember, EmberBulkRename, EmberDuplicateItem, EmberHi, EmberMoveItem}}; use crate::{Client, ID, PEERS, ember::{Ember, EmberBulkRename, EmberDuplicateItem, EmberHi, EmberMoveItem}};
@ -162,7 +162,7 @@ impl Pubsub {
pub_after!(rename(tab: Id, from: &UrlBuf, to: &UrlBuf), (tab, from, to)); pub_after!(rename(tab: Id, from: &UrlBuf, to: &UrlBuf), (tab, from, to));
pub_after!(@yank(cut: bool, urls: &IndexSet<UrlBufCov>), (cut, urls)); pub_after!(@yank(cut: bool, files: &IndexSet<FileCov>), (cut, files));
pub_after!(duplicate(items: Vec<EmberDuplicateItem>), (&items), (items)); pub_after!(duplicate(items: Vec<EmberDuplicateItem>), (&items), (items));

View file

@ -2,13 +2,14 @@ use std::{fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}};
use anyhow::bail; use anyhow::bail;
use mlua::FromLua; use mlua::FromLua;
use serde::{Deserialize, Serialize};
use yazi_macro::{unix_either, win_either}; use yazi_macro::{unix_either, win_either};
use yazi_shared::{strand::AsStrand, url::AsUrl}; use yazi_shared::{strand::AsStrand, url::AsUrl};
use super::ChaKind; use super::ChaKind;
use crate::cha::{ChaMode, ChaType}; use crate::cha::{ChaMode, ChaType};
#[derive(Clone, Copy, Debug, Eq, FromLua, PartialEq)] #[derive(Clone, Copy, Debug, Deserialize, Eq, FromLua, PartialEq, Serialize)]
pub struct Cha { pub struct Cha {
pub kind: ChaKind, pub kind: ChaKind,
pub mode: ChaMode, pub mode: ChaMode,

View file

@ -1,10 +1,11 @@
use std::fs::Metadata; use std::fs::Metadata;
use bitflags::bitflags; use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use yazi_shared::strand::AsStrand; use yazi_shared::strand::AsStrand;
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct ChaKind: u8 { pub struct ChaKind: u8 {
const FOLLOW = 0b0000_0001; const FOLLOW = 0b0000_0001;
const HIDDEN = 0b0000_0010; const HIDDEN = 0b0000_0010;

View file

@ -2,11 +2,12 @@ use std::ops::Deref;
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use bitflags::bitflags; use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use crate::cha::ChaType; use crate::cha::ChaType;
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct ChaMode: u16 { pub struct ChaMode: u16 {
// File type // File type
const T_MASK = 0b1111_0000_0000_0000; const T_MASK = 0b1111_0000_0000_0000;

View file

@ -7,7 +7,7 @@ use super::{FilesSorter, Filter};
use crate::{FILES_TICKET, SortBy, file::File}; use crate::{FILES_TICKET, SortBy, file::File};
#[derive(Default)] #[derive(Default)]
pub struct Files { pub struct Entries {
hidden: Vec<File>, hidden: Vec<File>,
items: Vec<File>, items: Vec<File>,
ticket: Id, ticket: Id,
@ -21,17 +21,17 @@ pub struct Files {
show_hidden: bool, show_hidden: bool,
} }
impl Deref for Files { impl Deref for Entries {
type Target = Vec<File>; type Target = Vec<File>;
fn deref(&self) -> &Self::Target { &self.items } fn deref(&self) -> &Self::Target { &self.items }
} }
impl DerefMut for Files { impl DerefMut for Entries {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items }
} }
impl Files { impl Entries {
pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } } pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } }
pub fn update_full(&mut self, files: Vec<File>) { pub fn update_full(&mut self, files: Vec<File>) {
@ -267,7 +267,7 @@ impl Files {
} }
} }
impl Files { impl Entries {
// --- Items // --- Items
#[inline] #[inline]
pub fn position(&self, urn: PathDyn) -> Option<usize> { self.iter().position(|f| urn == f.urn()) } pub fn position(&self, urn: PathDyn) -> Option<usize> { self.iter().position(|f| urn == f.urn()) }

23
yazi-fs/src/file/data.rs Normal file
View file

@ -0,0 +1,23 @@
use anyhow::anyhow;
use yazi_macro::impl_data_any;
use yazi_shared::data::Data;
use crate::file::File;
impl_data_any!(File, from_into_lua = inherit);
impl TryFrom<Data> for File {
type Error = anyhow::Error;
fn try_from(value: Data) -> Result<Self, Self::Error> {
value.into_any::<Self>().ok_or_else(|| anyhow!("not a File"))
}
}
impl TryFrom<&Data> for File {
type Error = anyhow::Error;
fn try_from(value: &Data) -> Result<Self, Self::Error> {
value.as_any::<Self>().cloned().ok_or_else(|| anyhow!("not a File"))
}
}

View file

@ -1,10 +1,11 @@
use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path}; use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path};
use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{UrlBuf, UrlLike}}; use serde::{Deserialize, Serialize};
use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}};
use crate::cha::{Cha, ChaType}; use crate::cha::{Cha, ChaType};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct File { pub struct File {
pub url: UrlBuf, pub url: UrlBuf,
pub cha: Cha, pub cha: Cha,
@ -29,6 +30,14 @@ impl<'a> From<&'a File> for Cow<'a, File> {
fn from(value: &'a File) -> Self { Cow::Borrowed(value) } fn from(value: &'a File) -> Self { Cow::Borrowed(value) }
} }
impl AsUrl for File {
fn as_url(&self) -> Url<'_> { self.url.as_url() }
}
impl AsUrl for &File {
fn as_url(&self) -> Url<'_> { self.url.as_url() }
}
impl File { impl File {
#[inline] #[inline]
pub fn from_dummy(url: impl Into<UrlBuf>, r#type: Option<ChaType>) -> Self { pub fn from_dummy(url: impl Into<UrlBuf>, r#type: Option<ChaType>) -> Self {

View file

@ -0,0 +1,80 @@
use std::{hash::{Hash, Hasher}, ops::Deref};
use hashbrown::Equivalent;
use mlua::{FromLua, IntoLua, Lua, Value};
use serde::{Deserialize, Serialize};
use yazi_shared::url::{UrlBuf, UrlBufCov, UrlCov};
use crate::file::File;
/// A newtype around [`File`] that hashes and compares by URL only
/// (covariantly), via [`UrlCov`]. Useful as a key in [`IndexMap`] / [`HashMap`]
/// when the file's metadata should not affect identity.
#[derive(Clone, Debug, Default)]
pub struct FileCov(pub File);
impl Deref for FileCov {
type Target = File;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<File> for FileCov {
fn from(value: File) -> Self { Self(value) }
}
impl From<&File> for FileCov {
fn from(value: &File) -> Self { Self(value.clone()) }
}
impl From<FileCov> for File {
fn from(value: FileCov) -> Self { value.0 }
}
impl From<&FileCov> for File {
fn from(value: &FileCov) -> Self { value.0.clone() }
}
impl Hash for FileCov {
fn hash<H: Hasher>(&self, state: &mut H) { UrlCov::from(&self.url).hash(state); }
}
impl PartialEq for FileCov {
fn eq(&self, other: &Self) -> bool { UrlCov::from(&self.url).eq(&UrlCov::from(&other.url)) }
}
impl PartialEq<UrlBufCov> for FileCov {
fn eq(&self, other: &UrlBufCov) -> bool { UrlCov::from(&self.url).eq(other) }
}
impl PartialEq<UrlBuf> for FileCov {
fn eq(&self, other: &UrlBuf) -> bool { UrlCov::from(&self.url).eq(&UrlCov::from(other)) }
}
impl Eq for FileCov {}
impl Equivalent<FileCov> for UrlCov<'_> {
fn equivalent(&self, key: &FileCov) -> bool { self.eq(&UrlCov::from(&key.url)) }
}
impl Serialize for FileCov {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for FileCov {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
File::deserialize(deserializer).map(Self)
}
}
impl IntoLua for FileCov {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { self.0.into_lua(lua) }
}
impl FromLua for FileCov {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
File::from_lua(value, lua).map(Self)
}
}

30
yazi-fs/src/file/files.rs Normal file
View file

@ -0,0 +1,30 @@
use std::ops::{Deref, DerefMut};
use yazi_shared::url::UrlBuf;
use crate::file::File;
#[derive(Clone, Debug, Default)]
pub struct Files(pub Vec<File>);
impl Deref for Files {
type Target = Vec<File>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Files {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl From<Vec<File>> for Files {
fn from(value: Vec<File>) -> Self { Self(value) }
}
impl From<Files> for Vec<File> {
fn from(value: Files) -> Self { value.0 }
}
impl From<Files> for Vec<UrlBuf> {
fn from(value: Files) -> Self { value.0.into_iter().map(|f| f.url).collect() }
}

View file

@ -1,9 +1,10 @@
use mlua::UserDataRegistry; use mlua::{AnyUserData, UserDataRegistry};
use crate::file::File; use crate::file::File;
pub struct FileInventory { pub struct FileInventory {
pub register: fn(&mut UserDataRegistry<File>), pub register: fn(&mut UserDataRegistry<File>),
pub from_lua: fn(&AnyUserData) -> mlua::Result<File>,
} }
inventory::collect!(FileInventory); inventory::collect!(FileInventory);

View file

@ -1,4 +1,4 @@
use mlua::{AnyUserData, ExternalError, FromLua, Lua, ObjectLike, Table, UserData, UserDataMethods, UserDataRef, UserDataRegistry, Value}; use mlua::{AnyUserData, ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, UserDataRef, UserDataRegistry, Value};
use yazi_binding::{impl_file_fields, impl_file_methods}; use yazi_binding::{impl_file_fields, impl_file_methods};
use crate::file::{File, FileInventory}; use crate::file::{File, FileInventory};
@ -25,7 +25,19 @@ impl TryFrom<AnyUserData> for File {
type Error = mlua::Error; type Error = mlua::Error;
fn try_from(value: AnyUserData) -> Result<Self, Self::Error> { fn try_from(value: AnyUserData) -> Result<Self, Self::Error> {
Ok(if let Ok(me) = value.take::<Self>() { me } else { value.get("bare")? }) if let Ok(me) = value.take::<Self>() {
return Ok(me);
}
for inv in inventory::iter::<FileInventory>() {
match (inv.from_lua)(&value) {
Ok(file) => return Ok(file),
Err(mlua::Error::UserDataTypeMismatch) => continue,
Err(e) => return Err(e),
}
}
Err(mlua::Error::UserDataTypeMismatch)
} }
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(file inventory lua); yazi_macro::mod_flat!(data file_cov file files inventory lua);

View file

@ -1,7 +1,7 @@
use std::hash::{BuildHasher, Hash}; use std::hash::{BuildHasher, Hash};
use mlua::UserDataMethods; use mlua::UserDataMethods;
use yazi_shared::url::{AsUrl, UrlBufInventory}; use yazi_shared::url::{AsUrl, UrlBuf, UrlBufInventory};
use yazi_shim::Twox128; use yazi_shim::Twox128;
use crate::{cha::Cha, file::File}; use crate::{cha::Cha, file::File};
@ -14,7 +14,7 @@ impl FsHash64 for File {
fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
} }
impl<T: AsUrl> FsHash64 for T { impl FsHash64 for UrlBuf {
fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) } fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) }
} }

View file

@ -2,7 +2,7 @@ extern crate self as yazi_fs;
yazi_macro::mod_pub!(cha file mounts path provider); yazi_macro::mod_pub!(cha file mounts path provider);
yazi_macro::mod_flat!(cwd files filter fns hash op scheme sorter sorting splatter stage url xdg); yazi_macro::mod_flat!(cwd entries filter fns hash op scheme sorter sorting splatter stage url xdg);
pub fn init() { pub fn init() {
CWD.init(<_>::default()); CWD.init(<_>::default());

View file

@ -1,4 +1,4 @@
use std::path::Path; use std::{iter, path::Path};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use yazi_macro::{impl_data_any, relay}; use yazi_macro::{impl_data_any, relay};
@ -25,7 +25,6 @@ pub enum FilesOp {
impl_data_any!(FilesOp); impl_data_any!(FilesOp);
impl FilesOp { impl FilesOp {
#[inline]
pub fn cwd(&self) -> &UrlBuf { pub fn cwd(&self) -> &UrlBuf {
match self { match self {
Self::Full(u, ..) => u, Self::Full(u, ..) => u,
@ -41,7 +40,21 @@ impl FilesOp {
} }
} }
#[inline] pub fn files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
match self {
Self::Full(_, files, _) => Box::new(files.iter()),
Self::Part(_, files, _) => Box::new(files.iter()),
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()),
}
}
pub fn emit(self) { pub fn emit(self) {
yazi_shared::event::Event::Call(relay!(mgr:update_files).with_any("op", self).into()).emit(); yazi_shared::event::Event::Call(relay!(mgr:update_files).with_any("op", self).into()).emit();
} }
@ -134,7 +147,7 @@ impl FilesOp {
} }
} }
pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec<UrlBuf>, Vec<UrlBuf>) { pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec<UrlBuf>, Vec<File>) {
match self { match self {
Self::Deleting(cwd, urns) => { Self::Deleting(cwd, urns) => {
(urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) (urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![])
@ -144,7 +157,7 @@ impl FilesOp {
.filter(|&(u, f)| u != f.urn()) .filter(|&(u, f)| u != f.urn())
.filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) .filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f)))
.filter(|(u, _)| contains(u)) .filter(|(u, _)| contains(u))
.map(|(u, f)| (u, f.url_owned())) .map(|(u, f)| (u, f.clone()))
.unzip(), .unzip(),
_ => (vec![], vec![]), _ => (vec![], vec![]),
} }

View file

@ -99,10 +99,10 @@ macro_rules! impl_data_any {
} }
}; };
(@from_lua_inherit) => { (@from_lua_inherit) => {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Box<dyn ::yazi_shared::data::DataAny>> { fn from_lua(value: &mlua::Value, lua: &mlua::Lua) -> mlua::Result<Box<dyn ::yazi_shared::data::DataAny>> {
use mlua::FromLua; use mlua::FromLua;
<Self as FromLua>::from_lua(value, lua) <Self as FromLua>::from_lua(value.clone(), lua)
.map(|v| Box::new(v) as Box<dyn ::yazi_shared::data::DataAny>) .map(|v| Box::new(v) as Box<dyn ::yazi_shared::data::DataAny>)
} }
}; };

View file

@ -21,6 +21,7 @@ yazi_macro::mod_flat!(
paste paste
peek peek
remove remove
remove_do
rename rename
reveal reveal
search search

View file

@ -1,21 +1,19 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::ActionCow, url::UrlBuf}; use yazi_shared::event::ActionCow;
#[derive(Debug)] #[derive(Debug)]
pub struct RemoveForm { pub struct RemoveForm {
pub force: bool, pub force: bool,
pub permanently: bool, pub permanently: bool,
pub hovered: bool, pub hovered: bool,
pub targets: Vec<UrlBuf>,
} }
impl From<ActionCow> for RemoveForm { impl From<ActionCow> for RemoveForm {
fn from(mut a: ActionCow) -> Self { fn from(a: ActionCow) -> Self {
Self { Self {
force: a.bool("force"), force: a.bool("force"),
permanently: a.bool("permanently"), permanently: a.bool("permanently"),
hovered: a.bool("hovered"), hovered: a.bool("hovered"),
targets: a.take_any("targets").unwrap_or_default(),
} }
} }
} }

View file

@ -0,0 +1,25 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::ActionCow, url::UrlBuf};
#[derive(Debug)]
pub struct RemoveDoForm {
pub permanently: bool,
pub targets: Vec<UrlBuf>,
}
impl From<ActionCow> for RemoveDoForm {
fn from(mut a: ActionCow) -> Self {
Self {
permanently: a.bool("permanently"),
targets: a.take_any("targets").unwrap_or_default(),
}
}
}
impl FromLua for RemoveDoForm {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for RemoveDoForm {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -1,16 +1,17 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::ActionCow, url::UrlBuf}; use yazi_fs::file::File;
use yazi_shared::event::ActionCow;
#[derive(Debug)] #[derive(Debug)]
pub struct ToggleForm { pub struct ToggleForm {
pub url: Option<UrlBuf>, pub file: Option<File>,
pub state: Option<bool>, pub state: Option<bool>,
} }
impl From<ActionCow> for ToggleForm { impl From<ActionCow> for ToggleForm {
fn from(mut a: ActionCow) -> Self { fn from(mut a: ActionCow) -> Self {
Self { Self {
url: a.take_first().ok(), file: a.take_first().ok(),
state: match a.get("state") { state: match a.get("state") {
Ok("on") => Some(true), Ok("on") => Some(true),
Ok("off") => Some(false), Ok("off") => Some(false),

View file

@ -1,16 +1,17 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::ActionCow, url::UrlBuf}; use yazi_fs::file::File;
use yazi_shared::event::ActionCow;
#[derive(Debug)] #[derive(Debug)]
pub struct ToggleAllForm { pub struct ToggleAllForm {
pub urls: Vec<UrlBuf>, pub files: Vec<File>,
pub state: Option<bool>, pub state: Option<bool>,
} }
impl From<ActionCow> for ToggleAllForm { impl From<ActionCow> for ToggleAllForm {
fn from(mut a: ActionCow) -> Self { fn from(mut a: ActionCow) -> Self {
Self { Self {
urls: a.take_seq(), files: a.take_seq(),
state: match a.get("state") { state: match a.get("state") {
Ok("on") => Some(true), Ok("on") => Some(true),
Ok("off") => Some(false), Ok("off") => Some(false),
@ -21,7 +22,7 @@ impl From<ActionCow> for ToggleAllForm {
} }
impl From<Option<bool>> for ToggleAllForm { impl From<Option<bool>> for ToggleAllForm {
fn from(state: Option<bool>) -> Self { Self { urls: vec![], state } } fn from(state: Option<bool>) -> Self { Self { files: vec![], state } }
} }
impl FromLua for ToggleAllForm { impl FromLua for ToggleAllForm {

View file

@ -66,7 +66,7 @@ pub enum Spark<'a> {
Quit(crate::app::QuitForm), Quit(crate::app::QuitForm),
Refresh(crate::VoidForm), Refresh(crate::VoidForm),
Remove(crate::mgr::RemoveForm), Remove(crate::mgr::RemoveForm),
RemoveDo(crate::mgr::RemoveForm), RemoveDo(crate::mgr::RemoveDoForm),
Rename(crate::mgr::RenameForm), Rename(crate::mgr::RenameForm),
Reveal(crate::mgr::RevealForm), Reveal(crate::mgr::RevealForm),
Search(crate::mgr::SearchForm), Search(crate::mgr::SearchForm),
@ -419,7 +419,8 @@ try_from_spark!(crate::mgr::OpenDoForm, mgr:open_do);
try_from_spark!(crate::mgr::OpenForm, mgr:open); try_from_spark!(crate::mgr::OpenForm, mgr:open);
try_from_spark!(crate::mgr::PasteForm, mgr:paste); try_from_spark!(crate::mgr::PasteForm, mgr:paste);
try_from_spark!(crate::mgr::PeekForm, mgr:peek); try_from_spark!(crate::mgr::PeekForm, mgr:peek);
try_from_spark!(crate::mgr::RemoveForm, mgr:remove, mgr:remove_do); try_from_spark!(crate::mgr::RemoveForm, mgr:remove);
try_from_spark!(crate::mgr::RemoveDoForm, mgr:remove_do);
try_from_spark!(crate::mgr::RenameForm, mgr:rename); try_from_spark!(crate::mgr::RenameForm, mgr:rename);
try_from_spark!(crate::mgr::RevealForm, mgr:reveal); try_from_spark!(crate::mgr::RevealForm, mgr:reveal);
try_from_spark!(crate::mgr::SearchForm, mgr:search, mgr:search_do); try_from_spark!(crate::mgr::SearchForm, mgr:search, mgr:search_do);

View file

@ -2,8 +2,8 @@ local M = {}
function M.selected_uri_list() function M.selected_uri_list()
local paths = {} local paths = {}
for _, u in pairs(cx.active.selected) do for _, f in pairs(cx.active.selected) do
paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(u.path)) paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(f.path))
end end
if #paths == 0 and cx.active.current.hovered then if #paths == 0 and cx.active.current.hovered then
paths[1] = "file://" .. ya.percent_encode(tostring(cx.active.current.hovered.path)) paths[1] = "file://" .. ya.percent_encode(tostring(cx.active.current.hovered.path))

View file

@ -2,8 +2,8 @@ local M = {}
local state = ya.sync(function() local state = ya.sync(function()
local selected = {} local selected = {}
for _, url in pairs(cx.active.selected) do for _, f in pairs(cx.active.selected) do
selected[#selected + 1] = url selected[#selected + 1] = f.url
end end
return cx.active.current.cwd, selected return cx.active.current.cwd, selected
end) end)
@ -25,12 +25,20 @@ function M:entry()
end end
local urls = M.split_urls(cwd, output) local urls = M.split_urls(cwd, output)
if #urls == 1 then if #urls == 0 then
return
elseif #urls == 1 then
local cha = #selected == 0 and fs.cha(urls[1]) local cha = #selected == 0 and fs.cha(urls[1])
ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true }) return ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true })
elseif #urls > 1 then end
urls.state = #selected > 0 and "off" or "on"
ya.emit("toggle_all", urls) local files = {}
for _, url in ipairs(urls) do
files[#files + 1] = fs.file(url)
end
if #files > 0 then
files.state = #selected > 0 and "off" or "on"
ya.emit("toggle_all", files)
end end
end end
@ -69,11 +77,7 @@ function M.split_urls(cwd, output)
local t = {} local t = {}
for line in output:gmatch("[^\r\n]+") do for line in output:gmatch("[^\r\n]+") do
local u = Url(line) local u = Url(line)
if u.is_absolute then t[#t + 1] = u.is_absolute and u or cwd:join(u)
t[#t + 1] = u
else
t[#t + 1] = cwd:join(u)
end
end end
return t return t
end end

View file

@ -2,8 +2,8 @@ local M = {}
local selected = ya.sync(function() local selected = ya.sync(function()
local urls = {} local urls = {}
for _, u in pairs(cx.active.selected) do for _, f in pairs(cx.active.selected) do
urls[#urls + 1] = u urls[#urls + 1] = f.url
end end
return urls return urls
end) end)

View file

@ -19,6 +19,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"create" => create(lua)?, b"create" => create(lua)?,
b"cwd" => cwd(lua)?, b"cwd" => cwd(lua)?,
b"expand_url" => expand_url(lua)?, b"expand_url" => expand_url(lua)?,
b"file" => file(lua)?,
b"op" => op(lua)?, b"op" => op(lua)?,
b"partitions" => partitions(lua)?, b"partitions" => partitions(lua)?,
b"read_dir" => read_dir(lua)?, b"read_dir" => read_dir(lua)?,
@ -121,6 +122,15 @@ fn expand_url(lua: &Lua) -> mlua::Result<Function> {
}) })
} }
fn file(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
match File::new(&*url).await {
Ok(file) => file.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
})
}
fn op(lua: &Lua) -> mlua::Result<Function> { fn op(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, (name, t): (mlua::String, Table)| match &*name.as_bytes() { lua.create_function(|lua, (name, t): (mlua::String, Table)| match &*name.as_bytes() {
b"part" => super::FilesOp::part(lua, t), b"part" => super::FilesOp::part(lua, t),

View file

@ -36,9 +36,9 @@ impl MgrProxy {
emit!(Call(relay!(mgr:open_do).with_any("opt", opt))); emit!(Call(relay!(mgr:open_do).with_any("opt", opt)));
} }
pub fn remove_do(targets: Vec<UrlBuf>, permanently: bool) { pub fn remove_do(permanently: bool, targets: impl Into<Vec<UrlBuf>>) {
emit!(Call( emit!(Call(
relay!(mgr:remove_do).with("permanently", permanently).with_any("targets", targets) relay!(mgr:remove_do).with("permanently", permanently).with_any("targets", targets.into())
)); ));
} }

View file

@ -24,6 +24,7 @@ dyn-clone = { workspace = true }
foldhash = { workspace = true } foldhash = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
indexmap = { workspace = true }
inventory = { workspace = true } inventory = { workspace = true }
memchr = "2.8.2" memchr = "2.8.2"
mlua = { workspace = true } mlua = { workspace = true }

View file

@ -14,7 +14,7 @@ pub trait DataAny: Any + Send + Sync + DynClone {
fn to_lua(&self, lua: &Lua) -> mlua::Result<Value> { dyn_clone::clone_box(self).into_lua(lua) } fn to_lua(&self, lua: &Lua) -> mlua::Result<Value> { dyn_clone::clone_box(self).into_lua(lua) }
fn from_lua(_value: Value, _lua: &Lua) -> mlua::Result<Box<dyn DataAny>> fn from_lua(_value: &Value, _lua: &Lua) -> mlua::Result<Box<dyn DataAny>>
where where
Self: Sized, Self: Sized,
{ {

View file

@ -3,7 +3,7 @@ use mlua::{Lua, Value};
use crate::data::DataAny; use crate::data::DataAny;
pub struct DataInventory { pub struct DataInventory {
pub from_lua: fn(Value, &Lua) -> mlua::Result<Box<dyn DataAny>>, pub from_lua: fn(&Value, &Lua) -> mlua::Result<Box<dyn DataAny>>,
} }
inventory::collect!(DataInventory); inventory::collect!(DataInventory);

View file

@ -61,7 +61,7 @@ impl Sendable {
} }
for inv in inventory::iter::<DataInventory> { for inv in inventory::iter::<DataInventory> {
match (inv.from_lua)(value.clone(), lua) { match (inv.from_lua)(&value, lua) {
Ok(data) => return Ok(Data::Any(data)), Ok(data) => return Ok(Data::Any(data)),
Err(mlua::Error::UserDataTypeMismatch) => continue, Err(mlua::Error::UserDataTypeMismatch) => continue,
Err(e) => return Err(e), Err(e) => return Err(e),

View file

@ -1,6 +1,7 @@
use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}}; use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}};
use hashbrown::Equivalent; use hashbrown::Equivalent;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use yazi_codegen::FromLuaOwned; use yazi_codegen::FromLuaOwned;
use yazi_shim::wtf8::FromWtf8Vec; use yazi_shim::wtf8::FromWtf8Vec;
@ -189,3 +190,29 @@ impl PathBufDyn {
} }
} }
} }
impl Serialize for PathBufDyn {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct Shadow<'a> {
kind: PathKind,
path: &'a [u8],
}
let path = self.as_path();
Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for PathBufDyn {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Shadow {
kind: PathKind,
path: Vec<u8>,
}
let Shadow { kind, path } = Shadow::deserialize(deserializer)?;
Self::with(kind, path).map_err(de::Error::custom)
}
}

View file

@ -1,5 +1,9 @@
use serde::{Deserialize, Serialize};
use crate::scheme::SchemeKind; use crate::scheme::SchemeKind;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PathKind { pub enum PathKind {
Os, Os,
Unix, Unix,

View file

@ -1,6 +1,7 @@
use std::{hash::BuildHasher, path::{Path, PathBuf}}; use std::{hash::BuildHasher, path::{Path, PathBuf}};
use hashbrown::{HashMap, hash_map::EntryRef}; use hashbrown::{HashMap, hash_map::EntryRef};
use indexmap::{IndexMap, map::RawEntryApiV1};
use crate::{loc::Loc, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCow}}; use crate::{loc::Loc, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCow}};
@ -127,6 +128,28 @@ where
} }
} }
impl<V, S> UrlMapExt<V> for IndexMap<UrlBuf, V, S>
where
S: BuildHasher,
{
fn get_or_insert_default<U>(&mut self, url: U) -> &mut V
where
U: AsUrl,
V: Default,
{
self.get_or_insert_with(url, |_| Default::default())
}
fn get_or_insert_with<U, F>(&mut self, url: U, default: F) -> &mut V
where
U: AsUrl,
F: FnOnce(Url<'_>) -> V,
{
let url = url.as_url();
self.raw_entry_mut_v1().from_key(&url).or_insert_with(|| (url.into(), default(url))).1
}
}
// --- UrlCovMapExt // --- UrlCovMapExt
pub trait UrlCovMapExt<V> { pub trait UrlCovMapExt<V> {
fn get_or_insert_default(&mut self, url: UrlCov<'_>) -> &mut V fn get_or_insert_default(&mut self, url: UrlCov<'_>) -> &mut V

View file

@ -1,12 +1,12 @@
use std::io; use std::io;
use tokio::{select, sync::mpsc::{self, UnboundedReceiver}}; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}};
use yazi_fs::{Files, FilesOp, cha::Cha, file::File, mounts::PARTITIONS, provider::{DirReader, FileHolder}}; use yazi_fs::{Entries, FilesOp, cha::Cha, file::File, mounts::PARTITIONS, provider::{DirReader, FileHolder}};
use yazi_shared::url::UrlBuf; use yazi_shared::url::UrlBuf;
use crate::{VfsCha, VfsFile, VfsFilesOp, provider::{self, DirEntry}}; use crate::{VfsCha, VfsFile, VfsFilesOp, provider::{self, DirEntry}};
pub trait VfsFiles { pub trait VfsEntries {
fn from_dir(dir: &UrlBuf) -> impl Future<Output = io::Result<UnboundedReceiver<File>>>; fn from_dir(dir: &UrlBuf) -> impl Future<Output = io::Result<UnboundedReceiver<File>>>;
fn from_dir_bulk(dir: &UrlBuf) -> impl Future<Output = io::Result<Vec<File>>>; fn from_dir_bulk(dir: &UrlBuf) -> impl Future<Output = io::Result<Vec<File>>>;
@ -14,7 +14,7 @@ pub trait VfsFiles {
fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future<Output = Option<Cha>>; fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future<Output = Option<Cha>>;
} }
impl VfsFiles for Files { impl VfsEntries for Entries {
async fn from_dir(dir: &UrlBuf) -> std::io::Result<UnboundedReceiver<File>> { async fn from_dir(dir: &UrlBuf) -> std::io::Result<UnboundedReceiver<File>> {
let mut it = provider::read_dir(dir).await?; let mut it = provider::read_dir(dir).await?;
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();

View file

@ -1,22 +1,35 @@
use anyhow::Result; use std::io;
use yazi_fs::{cha::Cha, file::File}; use yazi_fs::{cha::Cha, file::File};
use yazi_shared::url::{UrlBuf, UrlCow}; use yazi_shared::url::{UrlBuf, UrlCow};
use crate::{VfsCha, provider}; use crate::{VfsCha, provider};
pub trait VfsFile: Sized { pub trait VfsFile: Sized {
fn new<'a>(url: impl Into<UrlCow<'a>>) -> impl Future<Output = Result<Self>>; fn new<'a>(url: impl Into<UrlCow<'a>>) -> impl Future<Output = io::Result<Self>>;
fn maybe_new<'a>(url: impl Into<UrlCow<'a>>) -> impl Future<Output = io::Result<Option<Self>>>;
fn from_follow(url: UrlBuf, cha: Cha) -> impl Future<Output = Self>; fn from_follow(url: UrlBuf, cha: Cha) -> impl Future<Output = Self>;
} }
impl VfsFile for File { impl VfsFile for File {
async fn new<'a>(url: impl Into<UrlCow<'a>>) -> Result<Self> { async fn new<'a>(url: impl Into<UrlCow<'a>>) -> io::Result<Self> {
let url = url.into(); let url = url.into();
let cha = provider::symlink_metadata(&url).await?; let cha = provider::symlink_metadata(&url).await?;
Ok(Self::from_follow(url.into_owned(), cha).await) Ok(Self::from_follow(url.into_owned(), cha).await)
} }
async fn maybe_new<'a>(url: impl Into<UrlCow<'a>>) -> io::Result<Option<Self>> {
let url = url.into();
let cha = match provider::symlink_metadata(&url).await {
Ok(cha) => cha,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(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 { 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() { provider::read_link(&url).await.ok() } else { None };

View file

@ -1,5 +1,5 @@
yazi_macro::mod_pub!(config provider); yazi_macro::mod_pub!(config provider);
yazi_macro::mod_flat!(cha file files fns op); yazi_macro::mod_flat!(cha entries file fns op);
pub fn init() { provider::init(); } pub fn init() { provider::init(); }