From b8ceaea9407b96a05eafbb752a877290ad50317b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 2 Jul 2026 02:57:12 +0800 Subject: [PATCH] feat!: show file icons in trash/delete/overwrite confirmations (#4096) --- CHANGELOG.md | 3 + Cargo.lock | 31 +-- Cargo.toml | 2 +- yazi-actor/Cargo.toml | 1 + yazi-actor/src/lives/{files.rs => entries.rs} | 12 +- yazi-actor/src/lives/file.rs | 31 ++- yazi-actor/src/lives/folder.rs | 8 +- yazi-actor/src/lives/mod.rs | 2 +- yazi-actor/src/lives/preview.rs | 2 +- yazi-actor/src/lives/selected.rs | 6 +- yazi-actor/src/lives/yanked.rs | 4 +- yazi-actor/src/mgr/bulk_rename.rs | 2 +- yazi-actor/src/mgr/copy.rs | 2 +- yazi-actor/src/mgr/create.rs | 18 +- yazi-actor/src/mgr/escape.rs | 9 +- yazi-actor/src/mgr/filter_do.rs | 2 +- yazi-actor/src/mgr/find_arrow.rs | 4 +- yazi-actor/src/mgr/find_do.rs | 4 +- yazi-actor/src/mgr/hidden.rs | 4 +- yazi-actor/src/mgr/mod.rs | 1 + yazi-actor/src/mgr/open.rs | 4 +- yazi-actor/src/mgr/paste.rs | 2 +- yazi-actor/src/mgr/refresh.rs | 10 +- yazi-actor/src/mgr/remove.rs | 48 +---- yazi-actor/src/mgr/remove_do.rs | 28 +++ yazi-actor/src/mgr/rename.rs | 28 ++- yazi-actor/src/mgr/sort.rs | 6 +- yazi-actor/src/mgr/toggle.rs | 8 +- yazi-actor/src/mgr/toggle_all.rs | 18 +- yazi-actor/src/mgr/update_files.rs | 6 +- yazi-actor/src/mgr/update_yanked.rs | 4 +- yazi-actor/src/mgr/yank.rs | 4 +- yazi-boot/Cargo.toml | 2 +- yazi-cli/Cargo.toml | 2 +- yazi-config/src/inject.rs | 3 +- yazi-config/src/popup/options.rs | 47 +++-- yazi-core/src/mgr/mgr.rs | 4 +- yazi-core/src/mgr/yanked.rs | 54 ++--- yazi-core/src/tab/finder.rs | 24 +-- yazi-core/src/tab/folder.rs | 68 +++---- yazi-core/src/tab/preview.rs | 8 +- yazi-core/src/tab/selected.rs | 189 +++++++++--------- yazi-core/src/tab/tab.rs | 18 +- yazi-core/src/tasks/file.rs | 31 ++- yazi-core/src/tasks/prework.rs | 4 +- yazi-dds/src/ember/yank.rs | 34 ++-- yazi-dds/src/pubsub.rs | 6 +- yazi-fs/src/cha/cha.rs | 3 +- yazi-fs/src/cha/kind.rs | 3 +- yazi-fs/src/cha/mode.rs | 3 +- yazi-fs/src/{files.rs => entries.rs} | 10 +- yazi-fs/src/file/data.rs | 23 +++ yazi-fs/src/file/file.rs | 13 +- yazi-fs/src/file/file_cov.rs | 80 ++++++++ yazi-fs/src/file/files.rs | 30 +++ yazi-fs/src/file/inventory.rs | 3 +- yazi-fs/src/file/lua.rs | 16 +- yazi-fs/src/file/mod.rs | 2 +- yazi-fs/src/hash.rs | 4 +- yazi-fs/src/lib.rs | 2 +- yazi-fs/src/op.rs | 23 ++- yazi-macro/src/data.rs | 4 +- yazi-parser/src/mgr/mod.rs | 1 + yazi-parser/src/mgr/remove.rs | 6 +- yazi-parser/src/mgr/remove_do.rs | 25 +++ yazi-parser/src/mgr/toggle.rs | 7 +- yazi-parser/src/mgr/toggle_all.rs | 9 +- yazi-parser/src/spark/spark.rs | 5 +- yazi-plugin/preset/plugins/dnd.lua | 4 +- yazi-plugin/preset/plugins/fzf.lua | 28 +-- yazi-plugin/preset/plugins/multi.lua | 4 +- yazi-plugin/src/fs/fs.rs | 10 + yazi-proxy/src/mgr.rs | 4 +- yazi-shared/Cargo.toml | 1 + yazi-shared/src/data/any.rs | 2 +- yazi-shared/src/data/inventory.rs | 2 +- yazi-shared/src/data/sendable.rs | 2 +- yazi-shared/src/path/buf.rs | 27 +++ yazi-shared/src/path/kind.rs | 4 + yazi-shared/src/url/traits.rs | 23 +++ yazi-vfs/src/{files.rs => entries.rs} | 6 +- yazi-vfs/src/file.rs | 19 +- yazi-vfs/src/lib.rs | 2 +- 83 files changed, 779 insertions(+), 439 deletions(-) rename yazi-actor/src/lives/{files.rs => entries.rs} (85%) create mode 100644 yazi-actor/src/mgr/remove_do.rs rename yazi-fs/src/{files.rs => entries.rs} (98%) create mode 100644 yazi-fs/src/file/data.rs create mode 100644 yazi-fs/src/file/file_cov.rs create mode 100644 yazi-fs/src/file/files.rs create mode 100644 yazi-parser/src/mgr/remove_do.rs rename yazi-vfs/src/{files.rs => entries.rs} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41888030..1e73c2a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Make help menu a command palette ([#4074]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - Context-aware icons for inputs ([#4080]) +- Show file icons in trash/delete/overwrite confirmations ([#4096]) - Dynamic keymap Lua API ([#4031]) - New `ui.Input` element ([#4040]) - 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 - Rename `` to `` ([#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]) - `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074]) - 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 [#4074]: https://github.com/sxyazi/yazi/pull/4074 [#4080]: https://github.com/sxyazi/yazi/pull/4080 +[#4096]: https://github.com/sxyazi/yazi/pull/4096 diff --git a/Cargo.lock b/Cargo.lock index 254299f3..7d31e334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0-rc.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ "aead", "aes", @@ -182,9 +182,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", "serde", @@ -628,9 +628,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +checksum = "97bf4965940c2382204c0ded6dd3dd48c0c4e872f1e76fb1bf94f45991a2cb6a" dependencies = [ "clap", ] @@ -1583,9 +1583,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "ctutils", "subtle", @@ -2556,11 +2556,12 @@ dependencies = [ [[package]] name = "pkcs5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" dependencies = [ "aes", + "aes-gcm", "cbc", "der", "pbkdf2", @@ -3831,9 +3832,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "libc", @@ -3853,9 +3854,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4704,6 +4705,7 @@ dependencies = [ "futures", "hashbrown 0.17.1", "indexmap 2.14.0", + "inventory", "libc", "mlua", "paste", @@ -5222,6 +5224,7 @@ dependencies = [ "foldhash", "futures", "hashbrown 0.17.1", + "indexmap 2.14.0", "inventory", "libc", "memchr", diff --git a/Cargo.toml b/Cargo.toml index f01ee634..b69c6aa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ debug = false [workspace.dependencies] ansi-to-tui = "8.0.1" anyhow = "1.0.103" -arc-swap = { version = "1.9.1", features = [ "serde" ] } +arc-swap = { version = "1.9.2", features = [ "serde" ] } base64 = "0.22.1" bitflags = { version = "2.13.0", features = [ "serde" ] } chrono = "0.4.45" diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 1015e74a..8c05e59e 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -45,6 +45,7 @@ either = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } +inventory = { workspace = true } mlua = { workspace = true } paste = { workspace = true } ratatui-core = { workspace = true } diff --git a/yazi-actor/src/lives/files.rs b/yazi-actor/src/lives/entries.rs similarity index 85% rename from yazi-actor/src/lives/files.rs rename to yazi-actor/src/lives/entries.rs index e67998ae..4737bfeb 100644 --- a/yazi-actor/src/lives/files.rs +++ b/yazi-actor/src/lives/entries.rs @@ -5,19 +5,19 @@ use yazi_shim::mlua::UserDataFieldsExt; use super::{File, Filter, Lives, PtrCell}; -pub(super) struct Files { +pub(super) struct Entries { window: Range, folder: PtrCell, tab: PtrCell, } -impl Deref for Files { - type Target = yazi_fs::Files; +impl Deref for Entries { + 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( window: Range, folder: &yazi_core::tab::Folder, @@ -27,7 +27,7 @@ impl Files { } } -impl UserData for Files { +impl UserData for Entries { fn add_fields>(fields: &mut F) { fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose()); } diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index adca9c5f..5a32c2cb 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -3,6 +3,7 @@ use std::{ops::Deref, ptr}; use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods}; use yazi_binding::{Range, style::Style}; use yazi_config::THEME; +use yazi_fs::file::FileInventory; use yazi_shared::{path::AsPath, url::UrlLike}; use super::{FILE_CACHE, Lives}; @@ -17,7 +18,7 @@ pub(super) struct File { impl Deref for 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 for File { @@ -32,14 +33,16 @@ impl File { ) -> mlua::Result { use hashbrown::hash_map::Entry; - Ok(match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.files[idx])) { - Entry::Occupied(oe) => oe.into_mut().clone(), - Entry::Vacant(ve) => { - let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?; - ve.insert(ud.clone()); - ud - } - }) + Ok( + match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.entries[idx])) { + Entry::Occupied(oe) => oe.into_mut().clone(), + Entry::Vacant(ve) => { + let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?; + ve.insert(ud.clone()); + ud + } + }, + ) } #[inline] @@ -49,7 +52,6 @@ impl File { impl UserData for File { fn add_fields>(fields: &mut F) { 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("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())) }); 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, ()| { 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::()?.clone()), + } +} diff --git a/yazi-actor/src/lives/folder.rs b/yazi-actor/src/lives/folder.rs index e7fa3e38..c090d7c6 100644 --- a/yazi-actor/src/lives/folder.rs +++ b/yazi-actor/src/lives/folder.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, UserData, UserDataFields}; use yazi_config::LAYOUT; use yazi_shim::mlua::UserDataFieldsExt; -use super::{File, Files, Lives, PtrCell}; +use super::{Entries, File, Lives, PtrCell}; pub(super) struct Folder { window: Range, @@ -28,7 +28,7 @@ impl Folder { Some(w) => w, None => { 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 { fn add_fields>(fields: &mut F) { 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_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("cursor", |_, me| Ok(me.cursor)); diff --git a/yazi-actor/src/lives/mod.rs b/yazi-actor/src/lives/mod.rs index ea093ddb..05b6599e 100644 --- a/yazi-actor/src/lives/mod.rs +++ b/yazi-actor/src/lives/mod.rs @@ -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() { unsafe { FILE_CACHE.get().write(std::mem::MaybeUninit::new(<_>::default())) }; diff --git a/yazi-actor/src/lives/preview.rs b/yazi-actor/src/lives/preview.rs index fee0997e..ea87071c 100644 --- a/yazi-actor/src/lives/preview.rs +++ b/yazi-actor/src/lives/preview.rs @@ -30,7 +30,7 @@ impl UserData for Preview { .hovered_folder() .map(|f| { 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() }); diff --git a/yazi-actor/src/lives/selected.rs b/yazi-actor/src/lives/selected.rs index 68694257..a9119a83 100644 --- a/yazi-actor/src/lives/selected.rs +++ b/yazi-actor/src/lives/selected.rs @@ -1,8 +1,6 @@ use mlua::AnyUserData; -use yazi_shared::url::UrlBuf; -use super::Lives; -use crate::lives::PtrCell; +use super::{Lives, PtrCell}; #[derive(Clone, Copy)] pub(super) struct Selected; @@ -12,7 +10,7 @@ impl Selected { let inner = PtrCell::from(inner); Lives::scoped_userdata(yazi_binding::Iter::new( - inner.as_static().values().map(UrlBuf::from), + inner.as_static().files().cloned(), Some(inner.len()), )) } diff --git a/yazi-actor/src/lives/yanked.rs b/yazi-actor/src/lives/yanked.rs index 1f30826c..38581bae 100644 --- a/yazi-actor/src/lives/yanked.rs +++ b/yazi-actor/src/lives/yanked.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods}; use yazi_binding::Iter; -use yazi_shared::url::UrlBuf; +use yazi_fs::file::File; use yazi_shim::mlua::get_metatable; use super::{Lives, PtrCell}; @@ -25,7 +25,7 @@ impl Yanked { Lives::scoped_userdata(Self { inner, iter: Lives::scoped_userdata(Iter::new( - inner.as_static().iter().map(UrlBuf::from), + inner.as_static().iter().map(File::from), Some(inner.len()), ))?, }) diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 0bd71e9b..ba001880 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -32,7 +32,7 @@ impl Actor for BulkRename { 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() { succ!(NotifyProxy::push_warn("Bulk rename", "No files selected")); } diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index f2b06391..8361da5a 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -20,7 +20,7 @@ impl Actor for Copy { let mut it = if form.hovered { Box::new(cx.hovered().map(|h| &h.url).into_iter()) } else { - cx.tab().selected_or_hovered() + cx.tab().selected_or_hovered_urls() } .peekable(); diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index e4ae292e..9128720b 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -9,7 +9,7 @@ use yazi_macro::{input, ok_or_not_found, succ}; use yazi_parser::mgr::CreateForm; use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, maybe_exists, provider}; +use yazi_vfs::{VfsFile, provider}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -34,22 +34,24 @@ impl Actor for Create { }; 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() { - 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 - && maybe_exists(&new).await - && !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await + && let Some(file) = File::maybe_new(&new).await? + && !ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await { - return; + return Ok(()); } 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!(); } diff --git a/yazi-actor/src/mgr/escape.rs b/yazi-actor/src/mgr/escape.rs index 85ce7b85..132bda3b 100644 --- a/yazi-actor/src/mgr/escape.rs +++ b/yazi-actor/src/mgr/escape.rs @@ -70,12 +70,11 @@ impl Actor for EscapeVisual { let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) }; render!(); - let urls: Vec<_> = - indices.into_iter().filter_map(|i| tab.current.files.get(i)).map(|f| &f.url).collect(); + let files: Vec<_> = indices.into_iter().filter_map(|i| tab.current.entries.get(i)).collect(); if !select { - tab.selected.remove_many(urls); - } else if urls.len() != tab.selected.add_many(urls) { + tab.selected.remove_many(files); + } else if files.len() != tab.selected.add_many(files) { NotifyProxy::push_warn( "Escape visual mode", "Some files cannot be selected, due to path nesting conflict.", @@ -96,7 +95,7 @@ impl Actor for EscapeFilter { const NAME: &str = "escape_filter"; fn act(cx: &mut Ctx, _: Self::Form) -> Result { - if cx.current_mut().files.filter().is_none() { + if cx.current_mut().entries.filter().is_none() { succ!(false); } diff --git a/yazi-actor/src/mgr/filter_do.rs b/yazi-actor/src/mgr/filter_do.rs index b1758234..0d76cf11 100644 --- a/yazi-actor/src/mgr/filter_do.rs +++ b/yazi-actor/src/mgr/filter_do.rs @@ -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 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) { act!(mgr:hover, cx, hovered)?; diff --git a/yazi-actor/src/mgr/find_arrow.rs b/yazi-actor/src/mgr/find_arrow.rs index 815567ba..ad4be900 100644 --- a/yazi-actor/src/mgr/find_arrow.rs +++ b/yazi-actor/src/mgr/find_arrow.rs @@ -18,9 +18,9 @@ impl Actor for FindArrow { render!(finder.catchup(&tab.current)); let offset = if form.prev { - finder.prev(&tab.current.files, tab.current.cursor, false) + finder.prev(&tab.current.entries, tab.current.cursor, false) } else { - finder.next(&tab.current.files, tab.current.cursor, false) + finder.next(&tab.current.entries, tab.current.cursor, false) }; if let Some(offset) = offset { diff --git a/yazi-actor/src/mgr/find_do.rs b/yazi-actor/src/mgr/find_do.rs index abc2cf1e..abe73439 100644 --- a/yazi-actor/src/mgr/find_do.rs +++ b/yazi-actor/src/mgr/find_do.rs @@ -24,9 +24,9 @@ impl Actor for FindDo { } let step = if opt.prev { - finder.prev(&cx.current().files, cx.current().cursor, true) + finder.prev(&cx.current().entries, cx.current().cursor, true) } else { - finder.next(&cx.current().files, cx.current().cursor, true) + finder.next(&cx.current().entries, cx.current().cursor, true) }; if let Some(step) = step { diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 66edccd3..6113805a 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -24,8 +24,8 @@ impl Actor for Hidden { render!(); false } else { - f.files.set_show_hidden(state); - render_and!(f.files.catchup_revision()) + f.entries.set_show_hidden(state); + render_and!(f.entries.catchup_revision()) } }; diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index dfbc3aae..0bee758f 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -33,6 +33,7 @@ yazi_macro::mod_flat!( quit refresh remove + remove_do rename reveal search diff --git a/yazi-actor/src/mgr/open.rs b/yazi-actor/src/mgr/open.rs index 2d789ed0..03daa337 100644 --- a/yazi-actor/src/mgr/open.rs +++ b/yazi-actor/src/mgr/open.rs @@ -27,7 +27,7 @@ impl Actor for Open { Quit::with_selected(cx.hovered().map(|h| &h.url)) } else { 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() } else { 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() { diff --git a/yazi-actor/src/mgr/paste.rs b/yazi-actor/src/mgr/paste.rs index b030474a..63037efe 100644 --- a/yazi-actor/src/mgr/paste.rs +++ b/yazi-actor/src/mgr/paste.rs @@ -20,7 +20,7 @@ impl Actor for Paste { if mgr.yanked.cut { 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) } else { succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow)); diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index 00b05022..5d4ae16f 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -1,10 +1,10 @@ use anyhow::Result; 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_parser::VoidForm; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFiles, VfsFilesOp}; +use yazi_vfs::{VfsEntries, VfsFilesOp}; use yazi_watcher::MgrProxy; use crate::{Actor, Ctx}; @@ -29,7 +29,7 @@ impl Actor for Refresh { act!(mgr:watch, cx)?; act!(mgr:update_paged, cx)?; - cx.tasks.prework_sorted(&cx.current().files); + cx.tasks.prework_sorted(&cx.current().entries); succ!(); } } @@ -44,9 +44,9 @@ impl Refresh { // TODO: performance improvement fn trigger_dirs(folders: &[&Folder]) { 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(), Err(e) => FilesOp::issue_error(&dir, e).await, } diff --git a/yazi-actor/src/mgr/remove.rs b/yazi-actor/src/mgr/remove.rs index 077f072f..75d409a4 100644 --- a/yazi-actor/src/mgr/remove.rs +++ b/yazi-actor/src/mgr/remove.rs @@ -1,7 +1,8 @@ use anyhow::Result; use yazi_config::popup::ConfirmCfg; +use yazi_fs::file::Files; use yazi_macro::{act, confirm, succ}; -use yazi_parser::mgr::RemoveForm; +use yazi_parser::mgr::{RemoveDoForm, RemoveForm}; use yazi_proxy::MgrProxy; use yazi_shared::data::Data; @@ -14,58 +15,31 @@ impl Actor for Remove { const NAME: &str = "remove"; - fn act(cx: &mut Ctx, mut form: Self::Form) -> Result { + fn act(cx: &mut Ctx, form: Self::Form) -> Result { act!(mgr:escape_visual, cx)?; - form.targets = if form.hovered { - cx.hovered().map_or(vec![], |h| vec![h.url.clone()]) + let targets = Files(if form.hovered { + cx.hovered().map_or(vec![], |h| vec![h.clone()]) } 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!(); } 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!( cx, - if form.permanently { - ConfirmCfg::delete(&form.targets) - } else { - ConfirmCfg::trash(&form.targets) - } + if form.permanently { ConfirmCfg::delete(&targets) } else { ConfirmCfg::trash(&targets) } )?; tokio::spawn(async move { if confirm.future().await { - MgrProxy::remove_do(form.targets, form.permanently); + MgrProxy::remove_do(form.permanently, targets); } }); 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 { - 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!(); - } -} diff --git a/yazi-actor/src/mgr/remove_do.rs b/yazi-actor/src/mgr/remove_do.rs new file mode 100644 index 00000000..c1672130 --- /dev/null +++ b/yazi-actor/src/mgr/remove_do.rs @@ -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 { + 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!(); + } +} diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 92202b2b..b654e33c 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Result, bail}; use yazi_config::{YAZI, popup::ConfirmCfg}; use yazi_dds::Pubsub; 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_proxy::{ConfirmProxy, MgrProxy}; 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_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))?; 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() { - return; + return Ok(()); } 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 { - Self::r#do(tab, old, new).await.ok(); - } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { - Self::r#do(tab, old, new).await.ok(); + if form.force || Self::try_ask(&old, &new).await? { + Self::r#do(tab, old, new).await?; } + Ok::<(), anyhow::Error>(()) }); succ!(); } @@ -98,6 +97,17 @@ impl Rename { Ok(()) } + async fn try_ask(old: &UrlBuf, new: &UrlBuf) -> Result { + 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 { if by == "all" { return String::new(); diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index 3e99eef7..270e2067 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -30,8 +30,8 @@ impl Actor for Sort { render!(); false } else { - f.files.set_sorter(sorter); - render_and!(f.files.catchup_revision()) + f.entries.set_sorter(sorter); + render_and!(f.entries.catchup_revision()) } }; @@ -41,7 +41,7 @@ impl Actor for Sort { { act!(mgr:hover, 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 diff --git a/yazi-actor/src/mgr/toggle.rs b/yazi-actor/src/mgr/toggle.rs index b9c129a4..f68285a0 100644 --- a/yazi-actor/src/mgr/toggle.rs +++ b/yazi-actor/src/mgr/toggle.rs @@ -15,14 +15,14 @@ impl Actor for Toggle { fn act(cx: &mut Ctx, form: Self::Form) -> Result { 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!(); }; let b = match form.state { - Some(true) => render_and!(tab.selected.add(&url)), - Some(false) => render_and!(tab.selected.remove(&url)) | true, - None => render_and!(tab.selected.remove(&url) || tab.selected.add(&url)), + Some(true) => render_and!(tab.selected.add(file)), + Some(false) => render_and!(tab.selected.remove(&file.url)) | true, + None => render_and!(tab.selected.remove(&file.url) || tab.selected.add(file)), }; if !b { diff --git a/yazi-actor/src/mgr/toggle_all.rs b/yazi-actor/src/mgr/toggle_all.rs index 1364abe5..9b9c8efd 100644 --- a/yazi-actor/src/mgr/toggle_all.rs +++ b/yazi-actor/src/mgr/toggle_all.rs @@ -17,14 +17,14 @@ impl Actor for ToggleAll { use either::Either::*; 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 { - Some(true) if form.urls.is_empty() => Left((vec![], it.collect())), - Some(true) => Right((vec![], form.urls)), - Some(false) if form.urls.is_empty() => Left((it.collect(), vec![])), - Some(false) => Right((form.urls, vec![])), - None if form.urls.is_empty() => Left(it.partition(|&u| tab.selected.contains(u))), - None => Right(form.urls.into_iter().partition(|u| tab.selected.contains(u))), + Some(true) if form.files.is_empty() => Left((vec![], it.collect())), + Some(true) => Right((vec![], form.files)), + Some(false) if form.files.is_empty() => Left((it.collect(), vec![])), + Some(false) => Right((form.files, vec![])), + None if form.files.is_empty() => Left(it.partition(|&f| tab.selected.contains(f))), + None => Right(form.files.into_iter().partition(|f| tab.selected.contains(f))), }; let warn = match either { @@ -34,14 +34,14 @@ impl Actor for ToggleAll { } Right((removal, addition)) => { 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 { NotifyProxy::push_warn( "Toggle all", - "Some files cannot be selected, due to path nesting conflict.", + "Some files cannot be selected due to path nesting conflict.", ); } succ!(); diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index 97b77ad5..ff4a7b3a 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -16,7 +16,7 @@ impl Actor for UpdateFiles { const NAME: &str = "update_files"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - 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(); for op in [form.op].into_iter().chain(linked) { @@ -28,7 +28,7 @@ impl Actor for UpdateFiles { act!(mgr:hidden, 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:peek, cx)?; act!(mgr:watch, cx)?; @@ -80,7 +80,7 @@ impl UpdateFiles { } if calc { - cx.tasks.prework_sorted(&cx.current().files); + cx.tasks.prework_sorted(&cx.current().entries); } succ!(); } diff --git a/yazi-actor/src/mgr/update_yanked.rs b/yazi-actor/src/mgr/update_yanked.rs index 60a0148a..ef9182fa 100644 --- a/yazi-actor/src/mgr/update_yanked.rs +++ b/yazi-actor/src/mgr/update_yanked.rs @@ -14,11 +14,11 @@ impl Actor for UpdateYanked { const NAME: &str = "update_yanked"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - if form.urls.is_empty() && cx.mgr.yanked.is_empty() { + if form.files.is_empty() && cx.mgr.yanked.is_empty() { 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!()); } } diff --git a/yazi-actor/src/mgr/yank.rs b/yazi-actor/src/mgr/yank.rs index 99ac6a6b..3180863e 100644 --- a/yazi-actor/src/mgr/yank.rs +++ b/yazi-actor/src/mgr/yank.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_core::mgr::Yanked; use yazi_macro::{act, render}; use yazi_parser::mgr::YankForm; -use yazi_shared::{data::Data, url::UrlBufCov}; +use yazi_shared::data::Data; use crate::{Actor, Ctx}; @@ -17,7 +17,7 @@ impl Actor for Yank { act!(mgr:escape_visual, cx)?; 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)); act!(mgr:escape_select, cx) diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 4cc27b8e..69e25c6d 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -30,6 +30,6 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } # External dependencies clap = { workspace = true } -clap_complete = "4.6.5" +clap_complete = "4.6.6" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 09e86f9b..a23384b3 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -42,7 +42,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.6.5" +clap_complete = "4.6.6" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" serde = { workspace = true } diff --git a/yazi-config/src/inject.rs b/yazi-config/src/inject.rs index 7039e902..71bdf3f9 100644 --- a/yazi-config/src/inject.rs +++ b/yazi-config/src/inject.rs @@ -10,6 +10,7 @@ inventory::submit! { // TODO: use a cache Ok(THEME.icon.matches(me, false)) }); - } + }, + from_lua: |_| Err(mlua::Error::UserDataTypeMismatch), } } diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index e9906020..7b2587b8 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -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 yazi_binding::position::Position; +use yazi_fs::file::File; 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 #[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::replace_number(&YAZI.confirm.trash_title, urls.len()), + Self::replace_number(&YAZI.confirm.trash_title, files.len()), YAZI.confirm.trash_position(), 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::replace_number(&YAZI.confirm.delete_title, urls.len()), + Self::replace_number(&YAZI.confirm.delete_title, files.len()), YAZI.confirm.delete_position(), 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( YAZI.confirm.overwrite_title.clone(), YAZI.confirm.overwrite_position(), 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), YAZI.confirm.quit_position(), 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 { "" }) } - fn truncate_list(it: I, len: usize, max: usize) -> Option> + fn truncate_lines(it: I, len: usize, max: usize) -> Option> where I: IntoIterator, I::Item: ToStrand, @@ -97,4 +100,22 @@ impl ConfirmCfg { } Some(Text::from_iter(lines)) } + + fn truncate_files(files: &[File], max: usize) -> Option> { + 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()) + } } diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index 6a96c988..99f60a3c 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -67,7 +67,7 @@ impl Splatable for Mgr { tab .checked_sub(1) .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())) .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) @@ -82,5 +82,5 @@ impl Splatable for Mgr { .map(|h| h.url.as_url()) } - fn yanked(&self) -> impl Iterator> { self.yanked.iter().map(|u| u.as_url()) } + fn yanked(&self) -> impl Iterator> { self.yanked.iter().map(|f| f.url.as_url()) } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index 22db97b9..babb92be 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -1,32 +1,34 @@ use std::ops::Deref; use hashbrown::HashSet; -use indexmap::IndexSet; +use indexmap::{IndexSet, set::MutableValues}; use yazi_dds::Pubsub; -use yazi_fs::FilesOp; +use yazi_fs::{FilesOp, file::FileCov}; use yazi_macro::err; -use yazi_shared::url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlLike}; +use yazi_shared::url::{Url, UrlBuf, UrlCov, UrlLike}; #[derive(Debug, Default)] pub struct Yanked { pub cut: bool, - urls: IndexSet, + files: IndexSet, version: u64, revision: u64, } impl Deref for Yanked { - type Target = IndexSet; + type Target = IndexSet; - fn deref(&self) -> &Self::Target { &self.urls } + fn deref(&self) -> &Self::Target { &self.files } } impl Yanked { - pub fn new(cut: bool, urls: IndexSet) -> Self { - Self { cut, urls, ..Default::default() } + pub fn new(cut: bool, files: IndexSet) -> Self { + Self { cut, files, ..Default::default() } } + pub fn urls(&self) -> impl Iterator { self.files.iter().map(|f| &f.url) } + pub fn remove_many<'a, I, T>(&mut self, urls: I) where I: IntoIterator, @@ -37,45 +39,47 @@ impl Yanked { return; } - let old = self.urls.len(); - self.urls.retain(|u| !urls.contains(&UrlCov::new(u))); - self.revision += (old != self.urls.len()) as u64; + let old = self.files.len(); + self.files.retain(|f| !urls.contains(&UrlCov::new(&f.url))); + self.revision += (old != self.files.len()) as u64; } pub fn clear(&mut self) { - if self.urls.is_empty() { + if self.files.is_empty() { return; } - self.urls.clear(); + self.files.clear(); self.revision += 1; } pub fn contains<'a>(&self, url: impl Into>) -> bool { - self.urls.contains(&UrlCov::new(url)) + self.files.contains(&UrlCov::new(url)) } pub fn contains_in(&self, dir: &UrlBuf) -> bool { - self.urls.iter().any(|u| { - let mut it = u.components(); + self.files.iter().any(|f| { + let mut it = f.url.components(); it.next_back().is_some() && 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) { let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); if !removal.is_empty() { - let old = self.urls.len(); - self.urls.retain(|u| !removal.contains(u)); - self.revision += (old != self.urls.len()) as u64; + let old = self.files.len(); + self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u))); + self.revision += (old != self.files.len()) as u64; } - if !addition.is_empty() { - let old = self.urls.len(); - self.urls.extend(addition.into_iter().map(UrlBufCov)); - self.revision += (old != self.urls.len()) as u64; + let old = self.files.len(); + self.files.extend(addition.into_iter().map(FileCov)); + 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; - err!(Pubsub::pub_after_yank(self.cut, &self.urls)); + err!(Pubsub::pub_after_yank(self.cut, &self.files)); true } } diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index 506eb86e..fa8111e0 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -1,6 +1,6 @@ use anyhow::Result; use hashbrown::HashMap; -use yazi_fs::{Files, Filter, FilterCase}; +use yazi_fs::{Entries, Filter, FilterCase}; use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; use crate::tab::Folder; @@ -26,10 +26,10 @@ impl Finder { }) } - pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option { - for i in !include as usize..files.len() { - let idx = (cursor + files.len() - i) % files.len(); - if let Some(s) = files[idx].name() + pub fn prev(&self, entries: &Entries, cursor: usize, include: bool) -> Option { + for i in !include as usize..entries.len() { + let idx = (cursor + entries.len() - i) % entries.len(); + if let Some(s) = entries[idx].name() && self.filter.matches(s) { return Some(idx as isize - cursor as isize); @@ -38,10 +38,10 @@ impl Finder { None } - pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option { - for i in !include as usize..files.len() { - let idx = (cursor + i) % files.len(); - if let Some(s) = files[idx].name() + pub fn next(&self, entries: &Entries, cursor: usize, include: bool) -> Option { + for i in !include as usize..entries.len() { + let idx = (cursor + i) % entries.len(); + if let Some(s) = entries[idx].name() && self.filter.matches(s) { return Some(idx as isize - cursor as isize); @@ -57,7 +57,7 @@ impl Finder { self.matched.clear(); 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)) { continue; } @@ -87,12 +87,12 @@ impl Finder { // --- Lock impl From<&Folder> for FinderLock { 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 for FinderLock { fn eq(&self, other: &Folder) -> bool { - self.revision == other.files.revision && self.cwd == other.url + self.revision == other.entries.revision && self.cwd == other.url } } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 470749b4..49d8d63a 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -2,7 +2,7 @@ use std::mem; use yazi_config::{LAYOUT, YAZI}; 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_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::{Scrollable, Step}; @@ -10,10 +10,10 @@ use yazi_widgets::{Scrollable, Step}; use crate::MgrProxy; pub struct Folder { - pub url: UrlBuf, - pub cha: Cha, - pub files: Files, - pub stage: FolderStage, + pub url: UrlBuf, + pub cha: Cha, + pub entries: Entries, + pub stage: FolderStage, pub offset: usize, pub cursor: usize, @@ -25,14 +25,14 @@ pub struct Folder { impl Default for Folder { fn default() -> Self { Self { - url: Default::default(), - cha: Default::default(), - files: Files::new(YAZI.mgr.show_hidden.get()), - stage: Default::default(), - offset: Default::default(), - cursor: Default::default(), - page: Default::default(), - trace: Default::default(), + url: Default::default(), + cha: Default::default(), + entries: Entries::new(YAZI.mgr.show_hidden.get()), + stage: Default::default(), + offset: Default::default(), + cursor: Default::default(), + page: Default::default(), + trace: Default::default(), } } } @@ -43,7 +43,7 @@ impl> From for Folder { impl Folder { 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 { FilesOp::Full(_, _, cha) => { (self.cha, self.stage) = (cha, FolderStage::Loaded); @@ -51,10 +51,10 @@ impl Folder { FilesOp::Part(_, ref files, _) if files.is_empty() => { (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; } - 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); } FilesOp::IOErr(_, ref err) => { @@ -65,23 +65,23 @@ impl Folder { let mut deleted = vec![]; match op { - FilesOp::Full(_, files, _) => self.files.update_full(files), - FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket), + FilesOp::Full(_, files, _) => self.entries.update_full(files), + FilesOp::Part(_, files, ticket) => self.entries.update_part(files, ticket), FilesOp::Done(..) => {} - FilesOp::Size(_, sizes) => self.files.update_size(sizes), - FilesOp::IOErr(..) => self.files.update_ioerr(), + FilesOp::Size(_, sizes) => self.entries.update_size(sizes), + FilesOp::IOErr(..) => self.entries.update_ioerr(), - FilesOp::Creating(_, files) => self.files.update_creating(files), - FilesOp::Deleting(_, urns) => deleted = self.files.update_deleting(urns), - FilesOp::Updating(_, files) => _ = self.files.update_updating(files), - FilesOp::Upserting(_, files) => self.files.update_upserting(files), + FilesOp::Creating(_, files) => self.entries.update_creating(files), + FilesOp::Deleting(_, urns) => deleted = self.entries.update_deleting(urns), + FilesOp::Updating(_, files) => _ = self.entries.update_updating(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.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 { @@ -93,7 +93,7 @@ impl Folder { } pub fn arrow(&mut self, step: impl Into) -> 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) } else { self.scroll(step) @@ -109,7 +109,7 @@ impl Folder { 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); self.retrace(); @@ -146,7 +146,7 @@ impl Folder { fn squeeze_offset(&mut self) -> bool { let old = self.offset; - let len = self.files.len(); + let len = self.entries.len(); let limit = LAYOUT.get().folder_limit(); let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff.get() as usize); @@ -164,23 +164,23 @@ impl Folder { impl Folder { #[inline] - pub fn hovered(&self) -> Option<&File> { self.files.get(self.cursor) } + pub fn hovered(&self) -> Option<&File> { self.entries.get(self.cursor) } #[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] { - let len = self.files.len(); + let len = self.entries.len(); let limit = LAYOUT.get().folder_limit(); let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1)); let end = ((page + 2) * limit).min(len); - &self.files[start..end] + &self.entries[start..end] } } 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() } diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index 0b6d7275..a05ecff5 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -4,11 +4,11 @@ use tokio::{pin, task::JoinHandle}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_adapter::ADAPTOR; 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_runner::{RUNNER, previewer::{PeekError, PeekJob}}; 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}; @@ -60,9 +60,9 @@ impl Preview { self.folder_lock = Some(wd.clone()); self.folder_loader.take().map(|h| h.abort()); self.folder_loader = Some(tokio::spawn(async move { - let Some(new) = 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, Err(e) => return FilesOp::issue_error(&wd, e).await, }; diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 105ba9e4..5805d022 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -1,13 +1,11 @@ -use std::ops::Deref; - use hashbrown::HashMap; -use indexmap::IndexMap; -use yazi_fs::FilesOp; -use yazi_shared::{timestamp_us, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCovMapExt}}; +use indexmap::{IndexMap, map::MutableKeys}; +use yazi_fs::{FilesOp, file::{File, FileCov}}; +use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlBuf, UrlBufCov, UrlCov, UrlCovMapExt, UrlLike, UrlMapExt}}; #[derive(Default)] pub struct Selected { - inner: IndexMap, + inner: IndexMap, parents: HashMap, } @@ -16,64 +14,70 @@ impl Selected { pub fn is_empty(&self) -> bool { self.inner.is_empty() } - pub fn values(&self) -> impl Iterator { self.inner.keys().map(Deref::deref) } + pub fn files(&self) -> impl Iterator { self.inner.keys().map(|f| &f.0) } - pub fn contains<'a>(&self, url: impl Into>) -> bool { - self.inner.contains_key(&UrlCov::new(url)) + pub fn urls(&self) -> impl Iterator { self.inner.keys().map(|f| &f.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>) -> bool { self.add_same([url]) == 1 } + pub fn add(&mut self, file: impl AsUrl + Into) -> bool { self.add_same([file]) == 1 } - pub fn add_many<'a, I, T>(&mut self, urls: I) -> usize + pub fn add_many(&mut self, files: I) -> usize where I: IntoIterator, - T: Into>, + T: AsUrl + Into, { let mut grouped: IndexMap<_, Vec<_>> = Default::default(); - for url in urls.into_iter().map(Into::into) { - if let Some(p) = url.parent() { - grouped.entry(p).or_default().push(url); + for file in files { + if let Some(p) = file.as_url().parent() { + grouped.get_or_insert_default(p).push(file); } } grouped.into_values().map(|v| self.add_same(v)).sum() } - fn add_same<'a, I, T>(&mut self, urls: I) -> usize + fn add_same(&mut self, files: I) -> usize where I: IntoIterator, - T: Into>, + T: AsUrl + Into, { // If it has appeared as a parent - let urls: Vec<_> = - urls.into_iter().map(UrlCov::new).filter(|u| !self.parents.contains_key(u)).collect(); - if urls.is_empty() { + let files: Vec<_> = + files.into_iter().filter(|f| !self.parents.contains_key(&UrlCov::new(f.as_url()))).collect(); + if files.is_empty() { return 0; } // If it has appeared as a child - let mut parent = urls[0].parent(); - let mut parents = vec![]; + let mut parent = files[0].as_url().parent(); while let Some(u) = parent { if self.inner.contains_key(&UrlCov::new(u)) { return 0; } - parent = u.parent(); - parents.push(u); } - let (now, len) = (timestamp_us(), self.inner.len()); - self.inner.extend(urls.iter().enumerate().map(|(i, u)| (u.into(), now + i as u64))); + // Mark the files as selected children with a timestamp + 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 { - *self.parents.get_or_insert_default(UrlCov::new(u)) += self.inner.len() - len; + // Update the parent counts + 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> + Clone) -> bool { - self.remove_same([url]) == 1 - } + pub fn remove(&mut self, url: impl AsUrl) -> bool { self.remove_same([url]) == 1 } pub fn remove_many<'a, I, T>(&mut self, urls: I) -> usize where @@ -95,21 +99,21 @@ impl Selected { affected } - fn remove_same<'a, I, T>(&mut self, urls: I) -> usize + fn remove_same(&mut self, urls: I) -> usize where I: IntoIterator, - T: Into> + Clone, + T: AsUrl, { - let mut it = urls.into_iter().peekable(); - let Some(first) = it.peek().cloned().map(UrlCov::new) else { return 0 }; + let mut it = urls.into_iter(); + 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 { return 0; } - // FIXME: use UrlCov::parent() instead - let mut parent = first.parent(); + let mut parent = first.as_url().parent(); while let Some(u) = parent { let n = self.parents.get_mut(&UrlCov::new(u)).unwrap(); @@ -134,23 +138,28 @@ impl Selected { self.remove_many(&removal); } 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)] mod tests { - use std::path::Path; + use std::{ffi::OsStr, path::Path}; use super::*; + fn f + ?Sized>(s: &S) -> File { File::from_dummy(Path::new(s), None) } + #[test] fn test_insert_non_conflicting() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a/b"))); - assert!(s.add(Path::new("/c/d"))); + assert!(s.add(f("/a/b"))); + assert!(s.add(f("/c/d"))); assert_eq!(s.inner.len(), 2); } @@ -158,24 +167,24 @@ mod tests { fn test_insert_conflicting_parent() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a"))); - assert!(!s.add(Path::new("/a/b"))); + assert!(s.add(f("/a"))); + assert!(!s.add(f("/a/b"))); } #[test] fn test_insert_conflicting_child() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a/b/c"))); - assert!(!s.add(Path::new("/a/b"))); - assert!(s.add(Path::new("/a/b/d"))); + assert!(s.add(f("/a/b/c"))); + assert!(!s.add(f("/a/b"))); + assert!(s.add(f("/a/b/d"))); } #[test] fn test_remove() { 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/b"))); assert!(!s.remove(Path::new("/a/b"))); @@ -184,92 +193,83 @@ mod tests { } #[test] - fn insert_many_success() { + fn add_many_success() { let mut s = Selected::default(); - assert_eq!( - 3, - s.add_same([ - Path::new("/parent/child1"), - Path::new("/parent/child2"), - Path::new("/parent/child3") - ]) - ); + assert_eq!(3, s.add_same([f("/parent/child1"), f("/parent/child2"), f("/parent/child3")])); } #[test] - fn insert_many_with_existing_parent_fails() { + fn add_many_with_existing_parent_fails() { let mut s = Selected::default(); - s.add(Path::new("/parent")); - assert_eq!(0, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); + s.add(f("/parent")); + assert_eq!(0, s.add_same([f("/parent/child1"), f("/parent/child2")])); } #[test] - fn insert_many_with_existing_child_fails() { + fn add_many_with_existing_child_fails() { let mut s = Selected::default(); - s.add(Path::new("/parent/child1")); - assert_eq!(2, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); + s.add(f("/parent/child1")); + assert_eq!(2, s.add_same([f("/parent/child1"), f("/parent/child2")])); } #[test] - fn insert_many_empty_urls_list() { + fn add_many_empty_files_list() { let mut s = Selected::default(); - assert_eq!(0, s.add_same([] as [Url; 0])); + assert_eq!(0, s.add_same([] as [File; 0])); } #[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(); - s.add(Path::new("/parent/child")); - assert_eq!( - 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")])); + s.add(f("/parent/child")); + assert_eq!(0, s.add_same([f("/parent/child/child1"), f("/parent/child/child2")])); } #[test] - fn insert_many_with_nested_child_fails() { + fn add_many_with_direct_parent_fails() { let mut s = Selected::default(); - s.add(Path::new("/a/b")); - assert_eq!(0, s.add_same([Path::new("/a")])); - assert_eq!(1, s.add_same([Path::new("/b"), Path::new("/a")])); + s.add(f("/a")); + assert_eq!(0, s.add_same([f("/a/b")])); } #[test] - fn insert_many_sibling_directories_success() { + fn add_many_with_nested_child_fails() { 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] - fn insert_many_with_grandchild_fails() { + fn add_many_sibling_directories_success() { let mut s = Selected::default(); - s.add(Path::new("/a/b")); - assert_eq!(0, s.add_same([Path::new("/a/b/c")])); + assert_eq!(2, s.add_same([f("/a/b"), f("/a/c")])); } #[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 child1 = Path::new("/parent/child1"); let child2 = Path::new("/parent/child2"); 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_eq!(s.inner.len(), 2); @@ -282,4 +282,13 @@ mod tests { assert!(s.inner.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); + } } diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 8dd15cf3..6fd8660d 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -82,7 +82,7 @@ impl Tab { pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } pub fn hovered_rect(&self) -> Option { - let y = self.current.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; rect.y = rect.y.saturating_sub(1) + y as u16; @@ -99,22 +99,30 @@ impl Tab { } } - pub fn selected_or_hovered(&self) -> Box + '_> { + pub fn selected_or_hovered_files(&self) -> Box + '_> { + 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 + '_> { if self.selected.is_empty() { Box::new(self.hovered().map(|h| &h.url).into_iter()) } else { - Box::new(self.selected.values()) + Box::new(self.selected.urls()) } } pub fn hovered_and_selected(&self) -> Box + '_> { 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() { Box::new([&h.url, &h.url].into_iter()) } else { - Box::new([&h.url].into_iter().chain(self.selected.values())) + Box::new([&h.url].into_iter().chain(self.selected.urls())) } } diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index c956c76e..c92f9cc2 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -1,7 +1,6 @@ -use indexmap::IndexSet; use tracing::debug; use yazi_scheduler::file::FileInCut; -use yazi_shared::url::{UrlBuf, UrlBufCov, UrlLike}; +use yazi_shared::url::{UrlBuf, UrlLike}; use super::Tasks; use crate::mgr::Yanked; @@ -10,15 +9,15 @@ impl Tasks { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { 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 { debug!("file_cut: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_cut: same file, skip {to:?}"); } 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) { 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 { debug!("file_copy: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_copy: same file, skip {to:?}"); } 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, dest: &UrlBuf, relative: bool, force: bool) { + pub fn file_link(&self, src: &Yanked, dest: &UrlBuf, relative: bool, force: bool) { 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 { debug!("file_link: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_link: same file, skip {to:?}"); } 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, dest: &UrlBuf, force: bool, follow: bool) { + pub fn file_hardlink(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { 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 { debug!("file_hardlink: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_hardlink: same file, skip {to:?}"); } else { - self.scheduler.file_hardlink(u.0.clone(), to, force, follow); + self.scheduler.file_hardlink(u.clone(), to, force, follow); } } } diff --git a/yazi-core/src/tasks/prework.rs b/yazi-core/src/tasks/prework.rs index b1fa4523..be641480 100644 --- a/yazi-core/src/tasks/prework.rs +++ b/yazi-core/src/tasks/prework.rs @@ -1,5 +1,5 @@ use yazi_config::{YAZI, plugin::MAX_FETCHERS}; -use yazi_fs::{Files, FsHash64, SortBy, file::File}; +use yazi_fs::{Entries, FsHash64, SortBy, file::File}; use super::Tasks; 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 { return; } diff --git a/yazi-dds/src/ember/yank.rs b/yazi-dds/src/ember/yank.rs index 2150d5b3..dbf5e775 100644 --- a/yazi-dds/src/ember/yank.rs +++ b/yazi-dds/src/ember/yank.rs @@ -3,34 +3,31 @@ use std::borrow::Cow; use indexmap::IndexSet; use mlua::{AnyUserData, FromLua, IntoLua, Lua, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods, Value}; use serde::{Deserialize, Serialize}; +use yazi_fs::file::FileCov; use yazi_macro::impl_data_any; -use yazi_shared::url::{UrlBuf, UrlBufCov}; use yazi_shim::mlua::get_metatable; use super::Ember; -type Iter = yazi_binding::Iter< - std::iter::Map, fn(UrlBufCov) -> UrlBuf>, - UrlBuf, ->; +type Iter = yazi_binding::Iter, FileCov>; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct EmberYank<'a> { - pub cut: bool, - pub urls: Cow<'a, IndexSet>, + pub cut: bool, + pub files: Cow<'a, IndexSet>, } impl_data_any!(EmberYank<'static>, from_into_lua = inherit); impl<'a> EmberYank<'a> { - pub fn borrowed(cut: bool, urls: &'a IndexSet) -> Ember<'a> { - Self { cut, urls: Cow::Borrowed(urls) }.into() + pub fn borrowed(cut: bool, files: &'a IndexSet) -> Ember<'a> { + Self { cut, files: Cow::Borrowed(files) }.into() } } impl EmberYank<'static> { - pub fn owned(cut: bool, _: &IndexSet) -> Ember<'static> { - Self { cut, urls: Default::default() }.into() + pub fn owned(cut: bool, _: &IndexSet) -> Ember<'static> { + Self { cut, files: Default::default() }.into() } } @@ -53,8 +50,8 @@ impl FromLua for EmberYank<'static> { impl IntoLua for EmberYank<'_> { fn into_lua(self, lua: &Lua) -> mlua::Result { - let len = self.urls.len(); - let iter = Iter::new(self.urls.into_owned().into_iter().map(UrlBuf::from), Some(len)); + let len = self.files.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) } } @@ -69,15 +66,8 @@ pub struct EmberYankIter { impl EmberYankIter { pub fn collect(self, lua: &Lua) -> mlua::Result> { Ok(EmberYank { - cut: self.cut, - urls: Cow::Owned( - self - .inner - .take::()? - .into_iter(lua) - .map(|result| result.map(Into::into)) - .collect::>()?, - ), + cut: self.cut, + files: Cow::Owned(self.inner.take::()?.into_iter(lua).collect::>()?), }) } } diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index 42428a59..6fb77e3a 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -4,8 +4,8 @@ use indexmap::IndexSet; use mlua::Function; use parking_lot::RwLock; use yazi_boot::BOOT; -use yazi_fs::FolderStage; -use yazi_shared::{id::Id, url::{Url, UrlBuf, UrlBufCov}}; +use yazi_fs::{FolderStage, file::FileCov}; +use yazi_shared::{id::Id, url::{Url, UrlBuf}}; use yazi_shim::cell::RoCell; 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!(@yank(cut: bool, urls: &IndexSet), (cut, urls)); + pub_after!(@yank(cut: bool, files: &IndexSet), (cut, files)); pub_after!(duplicate(items: Vec), (&items), (items)); diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index deb11563..0ee64e82 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -2,13 +2,14 @@ use std::{fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; use anyhow::bail; use mlua::FromLua; +use serde::{Deserialize, Serialize}; use yazi_macro::{unix_either, win_either}; use yazi_shared::{strand::AsStrand, url::AsUrl}; use super::ChaKind; 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 kind: ChaKind, pub mode: ChaMode, diff --git a/yazi-fs/src/cha/kind.rs b/yazi-fs/src/cha/kind.rs index 7a20ce58..2bb4833c 100644 --- a/yazi-fs/src/cha/kind.rs +++ b/yazi-fs/src/cha/kind.rs @@ -1,10 +1,11 @@ use std::fs::Metadata; use bitflags::bitflags; +use serde::{Deserialize, Serialize}; use yazi_shared::strand::AsStrand; bitflags! { - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct ChaKind: u8 { const FOLLOW = 0b0000_0001; const HIDDEN = 0b0000_0010; diff --git a/yazi-fs/src/cha/mode.rs b/yazi-fs/src/cha/mode.rs index fc498bfb..fd2d2db7 100644 --- a/yazi-fs/src/cha/mode.rs +++ b/yazi-fs/src/cha/mode.rs @@ -2,11 +2,12 @@ use std::ops::Deref; use anyhow::{anyhow, bail}; use bitflags::bitflags; +use serde::{Deserialize, Serialize}; use crate::cha::ChaType; bitflags! { - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct ChaMode: u16 { // File type const T_MASK = 0b1111_0000_0000_0000; diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/entries.rs similarity index 98% rename from yazi-fs/src/files.rs rename to yazi-fs/src/entries.rs index f8ba5d95..84c7c426 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/entries.rs @@ -7,7 +7,7 @@ use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, SortBy, file::File}; #[derive(Default)] -pub struct Files { +pub struct Entries { hidden: Vec, items: Vec, ticket: Id, @@ -21,17 +21,17 @@ pub struct Files { show_hidden: bool, } -impl Deref for Files { +impl Deref for Entries { type Target = Vec; 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 } } -impl Files { +impl Entries { pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } } pub fn update_full(&mut self, files: Vec) { @@ -267,7 +267,7 @@ impl Files { } } -impl Files { +impl Entries { // --- Items #[inline] pub fn position(&self, urn: PathDyn) -> Option { self.iter().position(|f| urn == f.urn()) } diff --git a/yazi-fs/src/file/data.rs b/yazi-fs/src/file/data.rs new file mode 100644 index 00000000..17cf0e4b --- /dev/null +++ b/yazi-fs/src/file/data.rs @@ -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 for File { + type Error = anyhow::Error; + + fn try_from(value: Data) -> Result { + value.into_any::().ok_or_else(|| anyhow!("not a File")) + } +} + +impl TryFrom<&Data> for File { + type Error = anyhow::Error; + + fn try_from(value: &Data) -> Result { + value.as_any::().cloned().ok_or_else(|| anyhow!("not a File")) + } +} diff --git a/yazi-fs/src/file/file.rs b/yazi-fs/src/file/file.rs index 02191ada..1318dbf1 100644 --- a/yazi-fs/src/file/file.rs +++ b/yazi-fs/src/file/file.rs @@ -1,10 +1,11 @@ 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}; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct File { pub url: UrlBuf, 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) } } +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 { #[inline] pub fn from_dummy(url: impl Into, r#type: Option) -> Self { diff --git a/yazi-fs/src/file/file_cov.rs b/yazi-fs/src/file/file_cov.rs new file mode 100644 index 00000000..69fb7258 --- /dev/null +++ b/yazi-fs/src/file/file_cov.rs @@ -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 for FileCov { + fn from(value: File) -> Self { Self(value) } +} + +impl From<&File> for FileCov { + fn from(value: &File) -> Self { Self(value.clone()) } +} + +impl From 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(&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 for FileCov { + fn eq(&self, other: &UrlBufCov) -> bool { UrlCov::from(&self.url).eq(other) } +} + +impl PartialEq for FileCov { + fn eq(&self, other: &UrlBuf) -> bool { UrlCov::from(&self.url).eq(&UrlCov::from(other)) } +} + +impl Eq for FileCov {} + +impl Equivalent for UrlCov<'_> { + fn equivalent(&self, key: &FileCov) -> bool { self.eq(&UrlCov::from(&key.url)) } +} + +impl Serialize for FileCov { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for FileCov { + fn deserialize>(deserializer: D) -> Result { + File::deserialize(deserializer).map(Self) + } +} + +impl IntoLua for FileCov { + fn into_lua(self, lua: &Lua) -> mlua::Result { self.0.into_lua(lua) } +} + +impl FromLua for FileCov { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + File::from_lua(value, lua).map(Self) + } +} diff --git a/yazi-fs/src/file/files.rs b/yazi-fs/src/file/files.rs new file mode 100644 index 00000000..62434be8 --- /dev/null +++ b/yazi-fs/src/file/files.rs @@ -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); + +impl Deref for Files { + type Target = Vec; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Files { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl From> for Files { + fn from(value: Vec) -> Self { Self(value) } +} + +impl From for Vec { + fn from(value: Files) -> Self { value.0 } +} + +impl From for Vec { + fn from(value: Files) -> Self { value.0.into_iter().map(|f| f.url).collect() } +} diff --git a/yazi-fs/src/file/inventory.rs b/yazi-fs/src/file/inventory.rs index 624fbd0c..f729fe04 100644 --- a/yazi-fs/src/file/inventory.rs +++ b/yazi-fs/src/file/inventory.rs @@ -1,9 +1,10 @@ -use mlua::UserDataRegistry; +use mlua::{AnyUserData, UserDataRegistry}; use crate::file::File; pub struct FileInventory { pub register: fn(&mut UserDataRegistry), + pub from_lua: fn(&AnyUserData) -> mlua::Result, } inventory::collect!(FileInventory); diff --git a/yazi-fs/src/file/lua.rs b/yazi-fs/src/file/lua.rs index a6e93728..0f809112 100644 --- a/yazi-fs/src/file/lua.rs +++ b/yazi-fs/src/file/lua.rs @@ -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 crate::file::{File, FileInventory}; @@ -25,7 +25,19 @@ impl TryFrom for File { type Error = mlua::Error; fn try_from(value: AnyUserData) -> Result { - Ok(if let Ok(me) = value.take::() { me } else { value.get("bare")? }) + if let Ok(me) = value.take::() { + return Ok(me); + } + + for inv in inventory::iter::() { + match (inv.from_lua)(&value) { + Ok(file) => return Ok(file), + Err(mlua::Error::UserDataTypeMismatch) => continue, + Err(e) => return Err(e), + } + } + + Err(mlua::Error::UserDataTypeMismatch) } } diff --git a/yazi-fs/src/file/mod.rs b/yazi-fs/src/file/mod.rs index 69b498b3..157a9f22 100644 --- a/yazi-fs/src/file/mod.rs +++ b/yazi-fs/src/file/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(file inventory lua); +yazi_macro::mod_flat!(data file_cov file files inventory lua); diff --git a/yazi-fs/src/hash.rs b/yazi-fs/src/hash.rs index 852abadb..16df2a42 100644 --- a/yazi-fs/src/hash.rs +++ b/yazi-fs/src/hash.rs @@ -1,7 +1,7 @@ use std::hash::{BuildHasher, Hash}; use mlua::UserDataMethods; -use yazi_shared::url::{AsUrl, UrlBufInventory}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlBufInventory}; use yazi_shim::Twox128; 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) } } -impl FsHash64 for T { +impl FsHash64 for UrlBuf { fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) } } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index bfce7b20..d6532551 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -2,7 +2,7 @@ extern crate self as yazi_fs; 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() { CWD.init(<_>::default()); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index 4187f3c8..421f7452 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{iter, path::Path}; use hashbrown::{HashMap, HashSet}; use yazi_macro::{impl_data_any, relay}; @@ -25,7 +25,6 @@ pub enum FilesOp { impl_data_any!(FilesOp); impl FilesOp { - #[inline] pub fn cwd(&self) -> &UrlBuf { match self { Self::Full(u, ..) => u, @@ -41,7 +40,21 @@ impl FilesOp { } } - #[inline] + pub fn files(&self) -> Box + '_> { + 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) { 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, Vec) { + pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { match self { Self::Deleting(cwd, urns) => { (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_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) .filter(|(u, _)| contains(u)) - .map(|(u, f)| (u, f.url_owned())) + .map(|(u, f)| (u, f.clone())) .unzip(), _ => (vec![], vec![]), } diff --git a/yazi-macro/src/data.rs b/yazi-macro/src/data.rs index 80c3f56e..173dfcd5 100644 --- a/yazi-macro/src/data.rs +++ b/yazi-macro/src/data.rs @@ -99,10 +99,10 @@ macro_rules! impl_data_any { } }; (@from_lua_inherit) => { - fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result> { + fn from_lua(value: &mlua::Value, lua: &mlua::Lua) -> mlua::Result> { use mlua::FromLua; - ::from_lua(value, lua) + ::from_lua(value.clone(), lua) .map(|v| Box::new(v) as Box) } }; diff --git a/yazi-parser/src/mgr/mod.rs b/yazi-parser/src/mgr/mod.rs index 60bfeab5..188af862 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -21,6 +21,7 @@ yazi_macro::mod_flat!( paste peek remove + remove_do rename reveal search diff --git a/yazi-parser/src/mgr/remove.rs b/yazi-parser/src/mgr/remove.rs index cf41c029..0eb28b18 100644 --- a/yazi-parser/src/mgr/remove.rs +++ b/yazi-parser/src/mgr/remove.rs @@ -1,21 +1,19 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{event::ActionCow, url::UrlBuf}; +use yazi_shared::event::ActionCow; #[derive(Debug)] pub struct RemoveForm { pub force: bool, pub permanently: bool, pub hovered: bool, - pub targets: Vec, } impl From for RemoveForm { - fn from(mut a: ActionCow) -> Self { + fn from(a: ActionCow) -> Self { Self { force: a.bool("force"), permanently: a.bool("permanently"), hovered: a.bool("hovered"), - targets: a.take_any("targets").unwrap_or_default(), } } } diff --git a/yazi-parser/src/mgr/remove_do.rs b/yazi-parser/src/mgr/remove_do.rs new file mode 100644 index 00000000..4f8ed033 --- /dev/null +++ b/yazi-parser/src/mgr/remove_do.rs @@ -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, +} + +impl From 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 { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for RemoveDoForm { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/mgr/toggle.rs b/yazi-parser/src/mgr/toggle.rs index 168dcdb2..3dc34b17 100644 --- a/yazi-parser/src/mgr/toggle.rs +++ b/yazi-parser/src/mgr/toggle.rs @@ -1,16 +1,17 @@ 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)] pub struct ToggleForm { - pub url: Option, + pub file: Option, pub state: Option, } impl From for ToggleForm { fn from(mut a: ActionCow) -> Self { Self { - url: a.take_first().ok(), + file: a.take_first().ok(), state: match a.get("state") { Ok("on") => Some(true), Ok("off") => Some(false), diff --git a/yazi-parser/src/mgr/toggle_all.rs b/yazi-parser/src/mgr/toggle_all.rs index 21a121b6..c361e1ef 100644 --- a/yazi-parser/src/mgr/toggle_all.rs +++ b/yazi-parser/src/mgr/toggle_all.rs @@ -1,16 +1,17 @@ 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)] pub struct ToggleAllForm { - pub urls: Vec, + pub files: Vec, pub state: Option, } impl From for ToggleAllForm { fn from(mut a: ActionCow) -> Self { Self { - urls: a.take_seq(), + files: a.take_seq(), state: match a.get("state") { Ok("on") => Some(true), Ok("off") => Some(false), @@ -21,7 +22,7 @@ impl From for ToggleAllForm { } impl From> for ToggleAllForm { - fn from(state: Option) -> Self { Self { urls: vec![], state } } + fn from(state: Option) -> Self { Self { files: vec![], state } } } impl FromLua for ToggleAllForm { diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 054a883b..46745ac6 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -66,7 +66,7 @@ pub enum Spark<'a> { Quit(crate::app::QuitForm), Refresh(crate::VoidForm), Remove(crate::mgr::RemoveForm), - RemoveDo(crate::mgr::RemoveForm), + RemoveDo(crate::mgr::RemoveDoForm), Rename(crate::mgr::RenameForm), Reveal(crate::mgr::RevealForm), 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::PasteForm, mgr:paste); 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::RevealForm, mgr:reveal); try_from_spark!(crate::mgr::SearchForm, mgr:search, mgr:search_do); diff --git a/yazi-plugin/preset/plugins/dnd.lua b/yazi-plugin/preset/plugins/dnd.lua index 552c26de..b18ae139 100644 --- a/yazi-plugin/preset/plugins/dnd.lua +++ b/yazi-plugin/preset/plugins/dnd.lua @@ -2,8 +2,8 @@ local M = {} function M.selected_uri_list() local paths = {} - for _, u in pairs(cx.active.selected) do - paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(u.path)) + for _, f in pairs(cx.active.selected) do + paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(f.path)) end if #paths == 0 and cx.active.current.hovered then paths[1] = "file://" .. ya.percent_encode(tostring(cx.active.current.hovered.path)) diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index 6842d26a..8d5552e3 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -2,8 +2,8 @@ local M = {} local state = ya.sync(function() local selected = {} - for _, url in pairs(cx.active.selected) do - selected[#selected + 1] = url + for _, f in pairs(cx.active.selected) do + selected[#selected + 1] = f.url end return cx.active.current.cwd, selected end) @@ -25,12 +25,20 @@ function M:entry() end 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]) - ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true }) - elseif #urls > 1 then - urls.state = #selected > 0 and "off" or "on" - ya.emit("toggle_all", urls) + return ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true }) + end + + 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 @@ -69,11 +77,7 @@ function M.split_urls(cwd, output) local t = {} for line in output:gmatch("[^\r\n]+") do local u = Url(line) - if u.is_absolute then - t[#t + 1] = u - else - t[#t + 1] = cwd:join(u) - end + t[#t + 1] = u.is_absolute and u or cwd:join(u) end return t end diff --git a/yazi-plugin/preset/plugins/multi.lua b/yazi-plugin/preset/plugins/multi.lua index 9951140f..bf45ba89 100644 --- a/yazi-plugin/preset/plugins/multi.lua +++ b/yazi-plugin/preset/plugins/multi.lua @@ -2,8 +2,8 @@ local M = {} local selected = ya.sync(function() local urls = {} - for _, u in pairs(cx.active.selected) do - urls[#urls + 1] = u + for _, f in pairs(cx.active.selected) do + urls[#urls + 1] = f.url end return urls end) diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index b75c043b..7d7d8311 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -19,6 +19,7 @@ pub fn compose() -> Composer { b"create" => create(lua)?, b"cwd" => cwd(lua)?, b"expand_url" => expand_url(lua)?, + b"file" => file(lua)?, b"op" => op(lua)?, b"partitions" => partitions(lua)?, b"read_dir" => read_dir(lua)?, @@ -121,6 +122,15 @@ fn expand_url(lua: &Lua) -> mlua::Result { }) } +fn file(lua: &Lua) -> mlua::Result { + lua.create_async_function(|lua, url: UrlRef| async move { + match File::new(&*url).await { + Ok(file) => file.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }) +} + fn op(lua: &Lua) -> mlua::Result { lua.create_function(|lua, (name, t): (mlua::String, Table)| match &*name.as_bytes() { b"part" => super::FilesOp::part(lua, t), diff --git a/yazi-proxy/src/mgr.rs b/yazi-proxy/src/mgr.rs index dae29a97..9c9d8c15 100644 --- a/yazi-proxy/src/mgr.rs +++ b/yazi-proxy/src/mgr.rs @@ -36,9 +36,9 @@ impl MgrProxy { emit!(Call(relay!(mgr:open_do).with_any("opt", opt))); } - pub fn remove_do(targets: Vec, permanently: bool) { + pub fn remove_do(permanently: bool, targets: impl Into>) { 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()) )); } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 15e4434c..56cc7e1a 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -24,6 +24,7 @@ dyn-clone = { workspace = true } foldhash = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } +indexmap = { workspace = true } inventory = { workspace = true } memchr = "2.8.2" mlua = { workspace = true } diff --git a/yazi-shared/src/data/any.rs b/yazi-shared/src/data/any.rs index 4c4c159c..951eeb38 100644 --- a/yazi-shared/src/data/any.rs +++ b/yazi-shared/src/data/any.rs @@ -14,7 +14,7 @@ pub trait DataAny: Any + Send + Sync + DynClone { fn to_lua(&self, lua: &Lua) -> mlua::Result { dyn_clone::clone_box(self).into_lua(lua) } - fn from_lua(_value: Value, _lua: &Lua) -> mlua::Result> + fn from_lua(_value: &Value, _lua: &Lua) -> mlua::Result> where Self: Sized, { diff --git a/yazi-shared/src/data/inventory.rs b/yazi-shared/src/data/inventory.rs index b4114236..79f036d3 100644 --- a/yazi-shared/src/data/inventory.rs +++ b/yazi-shared/src/data/inventory.rs @@ -3,7 +3,7 @@ use mlua::{Lua, Value}; use crate::data::DataAny; pub struct DataInventory { - pub from_lua: fn(Value, &Lua) -> mlua::Result>, + pub from_lua: fn(&Value, &Lua) -> mlua::Result>, } inventory::collect!(DataInventory); diff --git a/yazi-shared/src/data/sendable.rs b/yazi-shared/src/data/sendable.rs index 39e0c8b4..e804ef5c 100644 --- a/yazi-shared/src/data/sendable.rs +++ b/yazi-shared/src/data/sendable.rs @@ -61,7 +61,7 @@ impl Sendable { } for inv in inventory::iter:: { - match (inv.from_lua)(value.clone(), lua) { + match (inv.from_lua)(&value, lua) { Ok(data) => return Ok(Data::Any(data)), Err(mlua::Error::UserDataTypeMismatch) => continue, Err(e) => return Err(e), diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 5abaedde..b0e0f20a 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}}; use hashbrown::Equivalent; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use yazi_codegen::FromLuaOwned; use yazi_shim::wtf8::FromWtf8Vec; @@ -189,3 +190,29 @@ impl PathBufDyn { } } } + +impl Serialize for PathBufDyn { + fn serialize(&self, serializer: S) -> Result { + #[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>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Shadow { + kind: PathKind, + path: Vec, + } + + let Shadow { kind, path } = Shadow::deserialize(deserializer)?; + Self::with(kind, path).map_err(de::Error::custom) + } +} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index c66781cd..fb2c9a1f 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -1,5 +1,9 @@ +use serde::{Deserialize, Serialize}; + use crate::scheme::SchemeKind; +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] pub enum PathKind { Os, Unix, diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index bb5d69fe..c1ea5387 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -1,6 +1,7 @@ use std::{hash::BuildHasher, path::{Path, PathBuf}}; use hashbrown::{HashMap, hash_map::EntryRef}; +use indexmap::{IndexMap, map::RawEntryApiV1}; use crate::{loc::Loc, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCow}}; @@ -127,6 +128,28 @@ where } } +impl UrlMapExt for IndexMap +where + S: BuildHasher, +{ + fn get_or_insert_default(&mut self, url: U) -> &mut V + where + U: AsUrl, + V: Default, + { + self.get_or_insert_with(url, |_| Default::default()) + } + + fn get_or_insert_with(&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 pub trait UrlCovMapExt { fn get_or_insert_default(&mut self, url: UrlCov<'_>) -> &mut V diff --git a/yazi-vfs/src/files.rs b/yazi-vfs/src/entries.rs similarity index 93% rename from yazi-vfs/src/files.rs rename to yazi-vfs/src/entries.rs index 7b5168b6..da47837b 100644 --- a/yazi-vfs/src/files.rs +++ b/yazi-vfs/src/entries.rs @@ -1,12 +1,12 @@ use std::io; 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 crate::{VfsCha, VfsFile, VfsFilesOp, provider::{self, DirEntry}}; -pub trait VfsFiles { +pub trait VfsEntries { fn from_dir(dir: &UrlBuf) -> impl Future>>; fn from_dir_bulk(dir: &UrlBuf) -> impl Future>>; @@ -14,7 +14,7 @@ pub trait VfsFiles { fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future>; } -impl VfsFiles for Files { +impl VfsEntries for Entries { async fn from_dir(dir: &UrlBuf) -> std::io::Result> { let mut it = provider::read_dir(dir).await?; let (tx, rx) = mpsc::unbounded_channel(); diff --git a/yazi-vfs/src/file.rs b/yazi-vfs/src/file.rs index 06d29965..26323a2d 100644 --- a/yazi-vfs/src/file.rs +++ b/yazi-vfs/src/file.rs @@ -1,22 +1,35 @@ -use anyhow::Result; +use std::io; + use yazi_fs::{cha::Cha, file::File}; use yazi_shared::url::{UrlBuf, UrlCow}; use crate::{VfsCha, provider}; pub trait VfsFile: Sized { - fn new<'a>(url: impl Into>) -> impl Future>; + fn new<'a>(url: impl Into>) -> impl Future>; + + fn maybe_new<'a>(url: impl Into>) -> impl Future>>; fn from_follow(url: UrlBuf, cha: Cha) -> impl Future; } impl VfsFile for File { - async fn new<'a>(url: impl Into>) -> Result { + async fn new<'a>(url: impl Into>) -> io::Result { let url = url.into(); let cha = provider::symlink_metadata(&url).await?; Ok(Self::from_follow(url.into_owned(), cha).await) } + async fn maybe_new<'a>(url: impl Into>) -> io::Result> { + let url = url.into(); + let cha = match provider::symlink_metadata(&url).await { + 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 { let link_to = if cha.is_link() { provider::read_link(&url).await.ok() } else { None }; diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index 498f440f..79d5c69a 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,5 +1,5 @@ yazi_macro::mod_pub!(config provider); -yazi_macro::mod_flat!(cha file files fns op); +yazi_macro::mod_flat!(cha entries file fns op); pub fn init() { provider::init(); }