mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat!: show file icons in trash/delete/overwrite confirmations (#4096)
This commit is contained in:
parent
21550a7eb5
commit
b8ceaea940
83 changed files with 779 additions and 439 deletions
|
|
@ -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
31
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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])) {
|
||||
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<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()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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())) };
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
))?,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ yazi_macro::mod_flat!(
|
|||
quit
|
||||
refresh
|
||||
remove
|
||||
remove_do
|
||||
rename
|
||||
reveal
|
||||
search
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
yazi-actor/src/mgr/remove_do.rs
Normal file
28
yazi-actor/src/mgr/remove_do.rs
Normal 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!();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ inventory::submit! {
|
|||
// TODO: use a cache
|
||||
Ok(THEME.icon.matches(me, false))
|
||||
});
|
||||
}
|
||||
},
|
||||
from_lua: |_| Err(mlua::Error::UserDataTypeMismatch),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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() }
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,64 +14,70 @@ 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()
|
||||
|
||||
fl
|
||||
}
|
||||
|
||||
pub fn remove<'a>(&mut self, url: impl Into<Url<'a>> + 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<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 cut: bool,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -69,15 +66,8 @@ pub struct EmberYankIter {
|
|||
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<_>>()?,
|
||||
),
|
||||
cut: self.cut,
|
||||
files: Cow::Owned(self.inner.take::<Iter>()?.into_iter(lua).collect::<mlua::Result<_>>()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
23
yazi-fs/src/file/data.rs
Normal 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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
80
yazi-fs/src/file/file_cov.rs
Normal file
80
yazi-fs/src/file/file_cov.rs
Normal 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
30
yazi-fs/src/file/files.rs
Normal 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() }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(file inventory lua);
|
||||
yazi_macro::mod_flat!(data file_cov file files inventory lua);
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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![]),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ yazi_macro::mod_flat!(
|
|||
paste
|
||||
peek
|
||||
remove
|
||||
remove_do
|
||||
rename
|
||||
reveal
|
||||
search
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
yazi-parser/src/mgr/remove_do.rs
Normal file
25
yazi-parser/src/mgr/remove_do.rs
Normal 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()) }
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue