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

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

View file

@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Make help menu a command palette ([#4074])
- 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 `<BackTab>` to `<S-Tab>` ([#3989])
- Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096])
- Remove `help:filter` action since the filter input is now always available ([#4074])
- `[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

31
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 }

View file

@ -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<usize>,
folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>,
}
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<usize>,
folder: &yazi_core::tab::Folder,
@ -27,7 +27,7 @@ impl Files {
}
}
impl UserData for Files {
impl UserData for Entries {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose());
}

View file

@ -3,6 +3,7 @@ use std::{ops::Deref, ptr};
use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use 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<yazi_fs::file::File> for File {
@ -32,14 +33,16 @@ impl File {
) -> mlua::Result<AnyUserData> {
use hashbrown::hash_map::Entry;
Ok(match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.files[idx])) {
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<F: UserDataFields<Self>>(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::<File>()?.clone()),
}
}

View file

@ -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<usize>,
@ -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<F: UserDataFields<Self>>(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));

View file

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

View file

@ -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()
});

View file

@ -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()),
))
}

View file

@ -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()),
))?,
})

View file

@ -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"));
}

View file

@ -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();

View file

@ -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!();
}

View file

@ -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<Data> {
if cx.current_mut().files.filter().is_none() {
if cx.current_mut().entries.filter().is_none() {
succ!(false);
}

View file

@ -17,7 +17,7 @@ impl Actor for FilterDo {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) };
let 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)?;

View file

@ -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 {

View file

@ -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 {

View file

@ -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())
}
};

View file

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

View file

@ -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() {

View file

@ -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));

View file

@ -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,
}

View file

@ -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<Data> {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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<Data> {
let mgr = &mut cx.mgr;
mgr.tabs.iter_mut().for_each(|t| {
t.selected.remove_many(&form.targets);
});
mgr.yanked.remove_many(&form.targets);
mgr.yanked.catchup_revision(false);
cx.tasks.file_remove(form.targets, form.permanently);
succ!();
}
}

View file

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

View file

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Result, bail};
use yazi_config::{YAZI, popup::ConfirmCfg};
use yazi_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<bool> {
let Some(file) = File::maybe_new(&new).await? else {
return Ok(true);
};
Ok(
provider::must_identical(old, new).await
|| ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await,
)
}
fn empty_url_part(url: &UrlBuf, by: &str) -> String {
if by == "all" {
return String::new();

View file

@ -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

View file

@ -15,14 +15,14 @@ impl Actor for Toggle {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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 {

View file

@ -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!();

View file

@ -16,7 +16,7 @@ impl Actor for UpdateFiles {
const NAME: &str = "update_files";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let revision = cx.current().files.revision;
let revision = cx.current().entries.revision;
let linked: Vec<_> = LINKED.read().from_dir(form.op.cwd()).map(|u| form.op.chdir(u)).collect();
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!();
}

View file

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

View file

@ -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)

View file

@ -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"

View file

@ -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 }

View file

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

View file

@ -1,10 +1,13 @@
use ratatui_core::text::{Line, Text};
use std::slice;
use ratatui_core::text::{Line, Span, Text};
use ratatui_widgets::paragraph::{Paragraph, Wrap};
use 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<I>(it: I, len: usize, max: usize) -> Option<Text<'static>>
fn truncate_lines<I>(it: I, len: usize, max: usize) -> Option<Text<'static>>
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<Text<'static>> {
let mut lines = Vec::with_capacity(files.len().min(max + 1));
for (i, f) in files.iter().enumerate() {
if i >= max {
lines.push(Line::raw(format!("... and {} more", files.len() - max)));
break;
}
lines.push(Line::default());
if let Some(icon) = THEME.icon.matches(f, false) {
lines[i].push_span(Span::styled(icon.text, icon.style));
lines[i].push_span(" ");
}
lines[i].push_span(f.url.to_strand().into_string_lossy());
}
Some(lines.into())
}
}

View file

@ -67,7 +67,7 @@ impl Splatable for Mgr {
tab
.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<Item = Url<'_>> { self.yanked.iter().map(|u| u.as_url()) }
fn yanked(&self) -> impl Iterator<Item = Url<'_>> { self.yanked.iter().map(|f| f.url.as_url()) }
}

View file

@ -1,32 +1,34 @@
use std::ops::Deref;
use 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<UrlBufCov>,
files: IndexSet<FileCov>,
version: u64,
revision: u64,
}
impl Deref for Yanked {
type Target = IndexSet<UrlBufCov>;
type Target = IndexSet<FileCov>;
fn deref(&self) -> &Self::Target { &self.urls }
fn deref(&self) -> &Self::Target { &self.files }
}
impl Yanked {
pub fn new(cut: bool, urls: IndexSet<UrlBufCov>) -> Self {
Self { cut, urls, ..Default::default() }
pub fn new(cut: bool, files: IndexSet<FileCov>) -> Self {
Self { cut, files, ..Default::default() }
}
pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { self.files.iter().map(|f| &f.url) }
pub fn remove_many<'a, I, T>(&mut self, urls: I)
where
I: IntoIterator<Item = T>,
@ -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<Url<'a>>) -> 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
}
}

View file

@ -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<isize> {
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<isize> {
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<isize> {
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<isize> {
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<Folder> 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
}
}

View file

@ -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};
@ -12,7 +12,7 @@ use crate::MgrProxy;
pub struct Folder {
pub url: UrlBuf,
pub cha: Cha,
pub files: Files,
pub entries: Entries,
pub stage: FolderStage,
pub offset: usize,
@ -27,7 +27,7 @@ impl Default for Folder {
Self {
url: Default::default(),
cha: Default::default(),
files: Files::new(YAZI.mgr.show_hidden.get()),
entries: Entries::new(YAZI.mgr.show_hidden.get()),
stage: Default::default(),
offset: Default::default(),
cursor: Default::default(),
@ -43,7 +43,7 @@ impl<T: Into<UrlBuf>> From<T> 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<Step>) -> bool {
let mut b = if self.files.is_empty() {
let mut b = if self.entries.is_empty() {
(mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0)
} 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() }

View file

@ -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,
};

View file

@ -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<UrlBufCov, u64>,
inner: IndexMap<FileCov, u64>,
parents: HashMap<UrlBufCov, usize>,
}
@ -16,65 +14,71 @@ impl Selected {
pub fn is_empty(&self) -> bool { self.inner.is_empty() }
pub fn values(&self) -> impl Iterator<Item = &UrlBuf> { self.inner.keys().map(Deref::deref) }
pub fn files(&self) -> impl Iterator<Item = &File> { self.inner.keys().map(|f| &f.0) }
pub fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool {
self.inner.contains_key(&UrlCov::new(url))
pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { 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<Url<'a>>) -> bool { self.add_same([url]) == 1 }
pub fn add(&mut self, file: impl AsUrl + Into<File>) -> bool { self.add_same([file]) == 1 }
pub fn add_many<'a, I, T>(&mut self, urls: I) -> usize
pub fn add_many<I, T>(&mut self, files: I) -> usize
where
I: IntoIterator<Item = T>,
T: Into<Url<'a>>,
T: AsUrl + Into<File>,
{
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<I, T>(&mut self, files: I) -> usize
where
I: IntoIterator<Item = T>,
T: Into<Url<'a>>,
T: AsUrl + Into<File>,
{
// 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()
}
pub fn remove<'a>(&mut self, url: impl Into<Url<'a>> + Clone) -> bool {
self.remove_same([url]) == 1
fl
}
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
I: IntoIterator<Item = T>,
@ -95,21 +99,21 @@ impl Selected {
affected
}
fn remove_same<'a, I, T>(&mut self, urls: I) -> usize
fn remove_same<I, T>(&mut self, urls: I) -> usize
where
I: IntoIterator<Item = T>,
T: Into<Url<'a>> + 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<S: AsRef<OsStr> + ?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);
}
}

View file

@ -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<Rect> {
let y = self.current.files.position(self.hovered()?.urn())? - self.current.offset;
let y = self.current.entries.position(self.hovered()?.urn())? - self.current.offset;
let mut rect = LAYOUT.get().current;
rect.y = rect.y.saturating_sub(1) + y as u16;
@ -99,22 +99,30 @@ impl Tab {
}
}
pub fn selected_or_hovered(&self) -> Box<dyn Iterator<Item = &UrlBuf> + '_> {
pub fn selected_or_hovered_files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
if self.selected.is_empty() {
Box::new(self.hovered().into_iter())
} else {
Box::new(self.selected.files())
}
}
pub fn selected_or_hovered_urls(&self) -> Box<dyn Iterator<Item = &UrlBuf> + '_> {
if self.selected.is_empty() {
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<dyn Iterator<Item = &UrlBuf> + '_> {
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()))
}
}

View file

@ -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<UrlBufCov>, 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<UrlBufCov>, 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);
}
}
}

View file

@ -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;
}

View file

@ -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<indexmap::set::IntoIter<UrlBufCov>, fn(UrlBufCov) -> UrlBuf>,
UrlBuf,
>;
type Iter = yazi_binding::Iter<indexmap::set::IntoIter<FileCov>, FileCov>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmberYank<'a> {
pub cut: bool,
pub urls: Cow<'a, IndexSet<UrlBufCov>>,
pub files: Cow<'a, IndexSet<FileCov>>,
}
impl_data_any!(EmberYank<'static>, from_into_lua = inherit);
impl<'a> EmberYank<'a> {
pub fn borrowed(cut: bool, urls: &'a IndexSet<UrlBufCov>) -> Ember<'a> {
Self { cut, urls: Cow::Borrowed(urls) }.into()
pub fn borrowed(cut: bool, files: &'a IndexSet<FileCov>) -> Ember<'a> {
Self { cut, files: Cow::Borrowed(files) }.into()
}
}
impl EmberYank<'static> {
pub fn owned(cut: bool, _: &IndexSet<UrlBufCov>) -> Ember<'static> {
Self { cut, urls: Default::default() }.into()
pub fn owned(cut: bool, _: &IndexSet<FileCov>) -> 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<Value> {
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)
}
}
@ -70,14 +67,7 @@ impl EmberYankIter {
pub fn collect(self, lua: &Lua) -> mlua::Result<EmberYank<'static>> {
Ok(EmberYank {
cut: self.cut,
urls: Cow::Owned(
self
.inner
.take::<Iter>()?
.into_iter(lua)
.map(|result| result.map(Into::into))
.collect::<mlua::Result<_>>()?,
),
files: Cow::Owned(self.inner.take::<Iter>()?.into_iter(lua).collect::<mlua::Result<_>>()?),
})
}
}

View file

@ -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<UrlBufCov>), (cut, urls));
pub_after!(@yank(cut: bool, files: &IndexSet<FileCov>), (cut, files));
pub_after!(duplicate(items: Vec<EmberDuplicateItem>), (&items), (items));

View file

@ -2,13 +2,14 @@ use std::{fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}};
use anyhow::bail;
use 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,

View file

@ -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;

View file

@ -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;

View file

@ -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<File>,
items: Vec<File>,
ticket: Id,
@ -21,17 +21,17 @@ pub struct Files {
show_hidden: bool,
}
impl Deref for Files {
impl Deref for Entries {
type Target = Vec<File>;
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<File>) {
@ -267,7 +267,7 @@ impl Files {
}
}
impl Files {
impl Entries {
// --- Items
#[inline]
pub fn position(&self, urn: PathDyn) -> Option<usize> { self.iter().position(|f| urn == f.urn()) }

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

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

View file

@ -1,10 +1,11 @@
use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path};
use 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<UrlBuf>, r#type: Option<ChaType>) -> Self {

View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
use std::hash::{BuildHasher, Hash};
use 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<T: AsUrl> FsHash64 for T {
impl FsHash64 for UrlBuf {
fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) }
}

View file

@ -2,7 +2,7 @@ extern crate self as yazi_fs;
yazi_macro::mod_pub!(cha file mounts path provider);
yazi_macro::mod_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());

View file

@ -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<dyn Iterator<Item = &File> + '_> {
match self {
Self::Full(_, files, _) => Box::new(files.iter()),
Self::Part(_, files, _) => Box::new(files.iter()),
Self::Done(..) => Box::new(iter::empty()),
Self::Size(..) => Box::new(iter::empty()),
Self::IOErr(..) => Box::new(iter::empty()),
Self::Creating(_, files) => Box::new(files.iter()),
Self::Deleting(..) => Box::new(iter::empty()),
Self::Updating(_, map) => Box::new(map.values()),
Self::Upserting(_, map) => Box::new(map.values()),
}
}
pub fn emit(self) {
yazi_shared::event::Event::Call(relay!(mgr:update_files).with_any("op", self).into()).emit();
}
@ -134,7 +147,7 @@ impl FilesOp {
}
}
pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec<UrlBuf>, Vec<UrlBuf>) {
pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec<UrlBuf>, Vec<File>) {
match self {
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![]),
}

View file

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

View file

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

View file

@ -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<UrlBuf>,
}
impl From<ActionCow> 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(),
}
}
}

View file

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

View file

@ -1,16 +1,17 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::ActionCow, url::UrlBuf};
use yazi_fs::file::File;
use yazi_shared::event::ActionCow;
#[derive(Debug)]
pub struct ToggleForm {
pub url: Option<UrlBuf>,
pub file: Option<File>,
pub state: Option<bool>,
}
impl From<ActionCow> 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),

View file

@ -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<UrlBuf>,
pub files: Vec<File>,
pub state: Option<bool>,
}
impl From<ActionCow> 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<ActionCow> for ToggleAllForm {
}
impl From<Option<bool>> for ToggleAllForm {
fn from(state: Option<bool>) -> Self { Self { urls: vec![], state } }
fn from(state: Option<bool>) -> Self { Self { files: vec![], state } }
}
impl FromLua for ToggleAllForm {

View file

@ -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);

View file

@ -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))

View file

@ -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

View file

@ -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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct Shadow<'a> {
kind: PathKind,
path: &'a [u8],
}
let path = self.as_path();
Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for PathBufDyn {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Shadow {
kind: PathKind,
path: Vec<u8>,
}
let Shadow { kind, path } = Shadow::deserialize(deserializer)?;
Self::with(kind, path).map_err(de::Error::custom)
}
}

View file

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

View file

@ -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<V, S> UrlMapExt<V> for IndexMap<UrlBuf, V, S>
where
S: BuildHasher,
{
fn get_or_insert_default<U>(&mut self, url: U) -> &mut V
where
U: AsUrl,
V: Default,
{
self.get_or_insert_with(url, |_| Default::default())
}
fn get_or_insert_with<U, F>(&mut self, url: U, default: F) -> &mut V
where
U: AsUrl,
F: FnOnce(Url<'_>) -> V,
{
let url = url.as_url();
self.raw_entry_mut_v1().from_key(&url).or_insert_with(|| (url.into(), default(url))).1
}
}
// --- UrlCovMapExt
pub trait UrlCovMapExt<V> {
fn get_or_insert_default(&mut self, url: UrlCov<'_>) -> &mut V

View file

@ -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<Output = io::Result<UnboundedReceiver<File>>>;
fn from_dir_bulk(dir: &UrlBuf) -> impl Future<Output = io::Result<Vec<File>>>;
@ -14,7 +14,7 @@ pub trait VfsFiles {
fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future<Output = Option<Cha>>;
}
impl VfsFiles for Files {
impl VfsEntries for Entries {
async fn from_dir(dir: &UrlBuf) -> std::io::Result<UnboundedReceiver<File>> {
let mut it = provider::read_dir(dir).await?;
let (tx, rx) = mpsc::unbounded_channel();

View file

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

View file

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