From eba9f1e4f0a25f7c21a5e5456bca7acaaa7711fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Tue, 29 Apr 2025 22:17:06 +0800 Subject: [PATCH] feat: calculate real-time directory size in spotter (#2695) --- yazi-binding/src/lib.rs | 2 +- yazi-binding/src/url.rs | 5 ++- yazi-binding/src/urn.rs | 32 ++++++++++++++ yazi-core/src/mgr/commands/update_mimes.rs | 2 +- yazi-core/src/spot/commands/close.rs | 6 +-- yazi-core/src/spot/spot.rs | 7 +++ yazi-core/src/tab/commands/update_spotted.rs | 13 ++++-- yazi-dds/src/sendable.rs | 31 ++++++++++--- yazi-fs/src/files.rs | 6 ++- yazi-plugin/preset/plugins/folder.lua | 46 +++++++++++++++++++- yazi-plugin/preset/ya.lua | 3 +- yazi-plugin/src/fs/fs.rs | 1 + yazi-plugin/src/fs/op.rs | 15 ++++++- yazi-plugin/src/isolate/peek.rs | 4 +- yazi-plugin/src/isolate/spot.rs | 6 ++- yazi-plugin/src/utils/spot.rs | 4 +- yazi-shared/src/event/data.rs | 9 +++- yazi-shared/src/url/urn.rs | 4 +- 18 files changed, 170 insertions(+), 26 deletions(-) create mode 100644 yazi-binding/src/urn.rs diff --git a/yazi-binding/src/lib.rs b/yazi-binding/src/lib.rs index 68afe73e..83b2fa91 100644 --- a/yazi-binding/src/lib.rs +++ b/yazi-binding/src/lib.rs @@ -1,3 +1,3 @@ mod macros; -yazi_macro::mod_flat!(error id stage url); +yazi_macro::mod_flat!(error id stage url urn); diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 81ae6e2a..41fd151e 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -2,7 +2,7 @@ use std::{ops::Deref, path::Path}; use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; -use crate::cached_field; +use crate::{Urn, cached_field}; pub type UrlRef = UserDataRef; @@ -12,6 +12,7 @@ pub struct Url { v_name: Option, v_stem: Option, v_ext: Option, + v_urn: Option, v_base: Option, v_parent: Option, v_frag: Option, @@ -39,6 +40,7 @@ impl Url { v_name: None, v_stem: None, v_ext: None, + v_urn: None, v_base: None, v_parent: None, v_frag: None, @@ -83,6 +85,7 @@ impl UserData for Url { me.extension().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent_url().map(Self::new))); + cached_field!(fields, urn, |_, me| Ok(Urn::new(me.urn_owned()))); cached_field!(fields, base, |_, me| { Ok(if me.base().as_os_str().is_empty() { None } else { Some(Self::new(me.base())) }) }); diff --git a/yazi-binding/src/urn.rs b/yazi-binding/src/urn.rs new file mode 100644 index 00000000..d30fcfa9 --- /dev/null +++ b/yazi-binding/src/urn.rs @@ -0,0 +1,32 @@ +use std::ops::Deref; + +use mlua::{ExternalError, FromLua, Lua, UserData, Value}; + +pub struct Urn { + inner: yazi_shared::url::UrnBuf, +} + +impl Deref for Urn { + type Target = yazi_shared::url::UrnBuf; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for yazi_shared::url::UrnBuf { + fn from(value: Urn) -> Self { value.inner } +} + +impl Urn { + pub fn new(urn: yazi_shared::url::UrnBuf) -> Self { Self { inner: urn } } +} + +impl FromLua for Urn { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + Ok(match value { + Value::UserData(ud) => ud.take()?, + _ => Err("Expected a Urn".into_lua_err())?, + }) + } +} + +impl UserData for Urn {} diff --git a/yazi-core/src/mgr/commands/update_mimes.rs b/yazi-core/src/mgr/commands/update_mimes.rs index 31103342..c10f6cb9 100644 --- a/yazi-core/src/mgr/commands/update_mimes.rs +++ b/yazi-core/src/mgr/commands/update_mimes.rs @@ -20,7 +20,7 @@ impl TryFrom for Opt { impl Mgr { pub fn update_mimes(&mut self, opt: impl TryInto, tasks: &Tasks) { - let Ok(opt) = opt.try_into() else { + let Ok(opt): Result = opt.try_into() else { return error!("invalid arguments for update_mimes"); }; diff --git a/yazi-core/src/spot/commands/close.rs b/yazi-core/src/spot/commands/close.rs index 31b403b6..c6fb6b52 100644 --- a/yazi-core/src/spot/commands/close.rs +++ b/yazi-core/src/spot/commands/close.rs @@ -1,4 +1,3 @@ -use yazi_macro::render; use yazi_shared::event::CmdCow; use crate::spot::Spot; @@ -14,8 +13,5 @@ impl From<()> for Opt { impl Spot { #[yazi_codegen::command] - pub fn close(&mut self, _: Opt) { - self.abort(); - render!(self.lock.take().is_some()); - } + pub fn close(&mut self, _: Opt) { self.reset(); } } diff --git a/yazi-core/src/spot/spot.rs b/yazi-core/src/spot/spot.rs index bf4ae609..fde9741c 100644 --- a/yazi-core/src/spot/spot.rs +++ b/yazi-core/src/spot/spot.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use tokio_util::sync::CancellationToken; use yazi_config::YAZI; use yazi_fs::File; +use yazi_macro::render; use yazi_plugin::{isolate, utils::SpotLock}; use yazi_shared::url::Url; @@ -36,6 +37,12 @@ impl Spot { #[inline] pub fn abort(&mut self) { self.ct.take().map(|ct| ct.cancel()); } + #[inline] + pub fn reset(&mut self) { + self.abort(); + render!(self.lock.take().is_some()); + } + #[inline] pub fn same_url(&self, url: &Url) -> bool { self.lock.as_ref().is_some_and(|l| *url == l.url) } diff --git a/yazi-core/src/tab/commands/update_spotted.rs b/yazi-core/src/tab/commands/update_spotted.rs index 9427cde9..f952d365 100644 --- a/yazi-core/src/tab/commands/update_spotted.rs +++ b/yazi-core/src/tab/commands/update_spotted.rs @@ -19,10 +19,10 @@ impl TryFrom for Opt { impl Tab { pub fn update_spotted(&mut self, opt: impl TryInto) { let Some(hovered) = self.hovered().map(|h| &h.url) else { - return self.preview.reset(); + return self.spot.reset(); }; - let Ok(opt) = opt.try_into() else { + let Ok(mut opt): Result = opt.try_into() else { return; }; @@ -30,7 +30,14 @@ impl Tab { return; } - self.spot.skip = opt.lock.selected().unwrap_or_default(); + if self.spot.lock.as_ref().is_none_or(|l| l.id != opt.lock.id) { + self.spot.skip = opt.lock.selected().unwrap_or_default(); + } else if let Some(s) = opt.lock.selected() { + self.spot.skip = s; + } else { + opt.lock.select(Some(self.spot.skip)); + } + self.spot.lock = Some(opt.lock); render!(); } diff --git a/yazi-dds/src/sendable.rs b/yazi-dds/src/sendable.rs index 4017151a..2b89ddab 100644 --- a/yazi-dds/src/sendable.rs +++ b/yazi-dds/src/sendable.rs @@ -43,6 +43,8 @@ impl Sendable { Value::UserData(ud) => { if let Ok(t) = ud.take::() { Data::Url(t.into()) + } else if let Ok(t) = ud.take::() { + Data::Urn(t.into()) } else if let Ok(t) = ud.borrow::() { Data::Id(**t) } else if let Ok(t) = ud.take::() { @@ -50,7 +52,7 @@ impl Sendable { } else if let Ok(t) = ud.take::() { Data::Any(Box::new(t)) } else { - Err("unsupported userdata included".into_lua_err())? + Err(format!("unsupported userdata included: {ud:?}").into_lua_err())? } } Value::Error(_) => Err("error is not supported".into_lua_err())?, @@ -76,12 +78,13 @@ impl Sendable { Value::Table(tbl) } Data::Url(u) => yazi_binding::Url::new(u).into_lua(lua)?, + Data::Urn(u) => yazi_binding::Urn::new(u).into_lua(lua)?, Data::Any(a) => Value::UserData(if a.is::() { lua.create_any_userdata(*a.downcast::().unwrap())? } else if a.is::() { lua.create_userdata(*a.downcast::().unwrap())? } else { - Err("unsupported userdata included".into_lua_err())? + Err("unsupported Data::Any included".into_lua_err())? }), data => Self::data_to_value_ref(lua, &data)?, }) @@ -111,13 +114,14 @@ impl Sendable { } Data::Id(i) => yazi_binding::Id(*i).into_lua(lua)?, Data::Url(u) => yazi_binding::Url::new(u.clone()).into_lua(lua)?, + Data::Urn(u) => yazi_binding::Urn::new(u.clone()).into_lua(lua)?, Data::Bytes(b) => Value::String(lua.create_string(b)?), Data::Any(a) => Value::UserData(if let Some(t) = a.downcast_ref::() { lua.create_any_userdata(t.clone())? } else if let Some(t) = a.downcast_ref::() { lua.create_userdata(t.clone())? } else { - Err("unsupported userdata included".into_lua_err())? + Err("unsupported Data::Any included".into_lua_err())? }), }) } @@ -191,7 +195,17 @@ impl Sendable { Value::Table(_) => Err("table is not supported".into_lua_err())?, Value::Function(_) => Err("function is not supported".into_lua_err())?, Value::Thread(_) => Err("thread is not supported".into_lua_err())?, - Value::UserData(_) => Err("userdata is not supported".into_lua_err())?, + Value::UserData(ud) => { + if let Ok(t) = ud.take::() { + DataKey::Url(t.into()) + } else if let Ok(t) = ud.take::() { + DataKey::Urn(t.into()) + } else if let Ok(t) = ud.borrow::() { + DataKey::Id(**t) + } else { + Err(format!("unsupported userdata included: {ud:?}").into_lua_err())? + } + } Value::Error(_) => Err("error is not supported".into_lua_err())?, Value::Other(..) => Err("unknown data is not supported".into_lua_err())?, }) @@ -199,7 +213,11 @@ impl Sendable { #[inline] fn key_to_value(lua: &Lua, key: DataKey) -> mlua::Result { - Self::key_to_value_ref(lua, &key) + match key { + DataKey::Url(u) => yazi_binding::Url::new(u).into_lua(lua), + DataKey::Urn(u) => yazi_binding::Urn::new(u).into_lua(lua), + _ => Self::key_to_value_ref(lua, &key), + } } fn key_to_value_ref(lua: &Lua, key: &DataKey) -> mlua::Result { @@ -209,6 +227,9 @@ impl Sendable { DataKey::Integer(i) => Value::Integer(*i), DataKey::Number(n) => Value::Number(n.get()), DataKey::String(s) => Value::String(lua.create_string(s.as_ref())?), + DataKey::Id(i) => yazi_binding::Id(*i).into_lua(lua)?, + DataKey::Url(u) => yazi_binding::Url::new(u.clone()).into_lua(lua)?, + DataKey::Urn(u) => yazi_binding::Urn::new(u.clone()).into_lua(lua)?, }) } } diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 344c8974..61a332ca 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -129,7 +129,11 @@ impl Files { } } - pub fn update_size(&mut self, sizes: HashMap) { + pub fn update_size(&mut self, mut sizes: HashMap) { + if sizes.len() <= 50 { + sizes.retain(|k, v| self.sizes.get(k) != Some(v)); + } + if sizes.is_empty() { return; } diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua index 6180e639..e52c4ce6 100644 --- a/yazi-plugin/preset/plugins/folder.lua +++ b/yazi-plugin/preset/plugins/folder.lua @@ -44,6 +44,50 @@ function M:seek(job) end end -function M:spot(job) require("file"):spot(job) end +function M:spot(job) + self.size, self.last = 0, 0 + + local url = job.file.url + local it = fs.calc_size(url) + while true do + local next = it:recv() + if next then + self.size = self.size + next + self:spot_multi(job, false) + else + break + end + end + + local op = fs.op("size", { url = url.parent, sizes = { [url.urn] = self.size } }) + ya.emit("update_files", { op = op }) + + self:spot_multi(job, true) +end + +function M:spot_multi(job, force) + local now = ya.time() + if not force and now < self.last + 0.1 then + return + end + + local rows = { + ui.Row({ "Folder" }):style(ui.Style():fg("green")), + ui.Row { " Size:", ya.readable_size(self.size) .. (force and "" or " (?)") }, + ui.Row {}, + } + + ya.spot_table( + job, + ui.Table(ya.list_merge(rows, require("file"):spot_base(job))) + :area(ui.Pos { "center", w = 60, h = 20 }) + :row(self.last == 0 and 1 or nil) + :col(1) + :col_style(th.spot.tbl_col) + :cell_style(th.spot.tbl_cell) + :widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) } + ) + self.last = now +end return M diff --git a/yazi-plugin/preset/ya.lua b/yazi-plugin/preset/ya.lua index da504d5a..2efa1732 100644 --- a/yazi-plugin/preset/ya.lua +++ b/yazi-plugin/preset/ya.lua @@ -33,7 +33,8 @@ function ya.readable_size(size) size = size / 1024 i = i + 1 end - return string.format("%.1f%s", size, units[i]):gsub("[.,]0", "", 1) + local s = string.format("%.1f%s", size, units[i]):gsub("[.,]0", "", 1) + return s end function ya.readable_path(path) diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index fd90d672..dbdcc805 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -30,6 +30,7 @@ fn op(lua: &Lua) -> mlua::Result { lua.create_function(|lua, (name, t): (mlua::String, Table)| match name.as_bytes().as_ref() { b"part" => super::FilesOp::part(lua, t), b"done" => super::FilesOp::done(lua, t), + b"size" => super::FilesOp::size(lua, t), _ => Err("Unknown operation".into_lua_err())?, }) } diff --git a/yazi-plugin/src/fs/op.rs b/yazi-plugin/src/fs/op.rs index e312eba5..1d6a338c 100644 --- a/yazi-plugin/src/fs/op.rs +++ b/yazi-plugin/src/fs/op.rs @@ -1,5 +1,5 @@ use mlua::{IntoLua, Lua, Table}; -use yazi_binding::{Id, Url}; +use yazi_binding::{Id, Url, Urn}; use crate::{bindings::Cha, file::File}; @@ -28,6 +28,19 @@ impl FilesOp { Ok(Self(yazi_fs::FilesOp::Done(url.into(), *cha, *id))) } + + pub(super) fn size(_: &Lua, t: Table) -> mlua::Result { + let url: Url = t.raw_get("url")?; + let sizes: Table = t.raw_get("sizes")?; + + Ok(Self(yazi_fs::FilesOp::Size( + url.into(), + sizes + .pairs::() + .map(|r| r.map(|(urn, size)| (urn.into(), size))) + .collect::>()?, + ))) + } } impl IntoLua for FilesOp { diff --git a/yazi-plugin/src/isolate/peek.rs b/yazi-plugin/src/isolate/peek.rs index 4866259c..3d9cd4c5 100644 --- a/yazi-plugin/src/isolate/peek.rs +++ b/yazi-plugin/src/isolate/peek.rs @@ -34,9 +34,9 @@ pub fn peek( _ = ct_.cancelled() => {}, Ok(b) = LOADER.ensure(&cmd.name, |c| c.sync_peek) => { if b { - peek_sync(cmd, file, mime, skip); + peek_sync( cmd, file, mime, skip); } else { - peek_async(cmd, file, mime, skip, ct_); + peek_async( cmd, file, mime, skip, ct_); } }, else => {} diff --git a/yazi-plugin/src/isolate/spot.rs b/yazi-plugin/src/isolate/spot.rs index 0dab49b1..85831e17 100644 --- a/yazi-plugin/src/isolate/spot.rs +++ b/yazi-plugin/src/isolate/spot.rs @@ -4,12 +4,15 @@ use mlua::{ExternalError, ExternalResult, HookTriggers, IntoLua, ObjectLike, VmS use tokio::{runtime::Handle, select}; use tokio_util::sync::CancellationToken; use tracing::error; +use yazi_binding::Id; use yazi_dds::Sendable; -use yazi_shared::event::Cmd; +use yazi_shared::{Ids, event::Cmd}; use super::slim_lua; use crate::{file::File, loader::LOADER}; +static IDS: Ids = Ids::new(); + pub fn spot( cmd: &'static Cmd, file: yazi_fs::File, @@ -37,6 +40,7 @@ pub fn spot( let plugin = LOADER.load_once(&lua, &cmd.name)?; let job = lua.create_table_from([ + ("id", Id(IDS.next()).into_lua(&lua)?), ("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?), ("file", File::new(file).into_lua(&lua)?), ("mime", mime.into_lua(&lua)?), diff --git a/yazi-plugin/src/utils/spot.rs b/yazi-plugin/src/utils/spot.rs index a9323a19..605c7064 100644 --- a/yazi-plugin/src/utils/spot.rs +++ b/yazi-plugin/src/utils/spot.rs @@ -1,7 +1,7 @@ use mlua::{AnyUserData, Function, Lua, Table}; use yazi_config::THEME; use yazi_macro::emit; -use yazi_shared::event::Cmd; +use yazi_shared::{Id, event::Cmd}; use super::Utils; use crate::{elements::Renderable, file::FileRef}; @@ -11,6 +11,7 @@ pub struct SpotLock { pub cha: yazi_fs::cha::Cha, pub mime: String, + pub id: Id, pub skip: usize, pub data: Vec, } @@ -25,6 +26,7 @@ impl TryFrom for SpotLock { cha: file.cha, mime: t.raw_get("mime")?, + id: *t.raw_get::("id")?, skip: t.raw_get("skip")?, data: Default::default(), }) diff --git a/yazi-shared/src/event/data.rs b/yazi-shared/src/event/data.rs index 3509d7fe..f2a9a251 100644 --- a/yazi-shared/src/event/data.rs +++ b/yazi-shared/src/event/data.rs @@ -2,7 +2,7 @@ use std::{any::Any, borrow::Cow, collections::HashMap}; use serde::{Deserialize, Serialize, de}; -use crate::{Id, OrderedFloat, url::Url}; +use crate::{Id, OrderedFloat, url::{Url, UrnBuf}}; // --- Data #[derive(Debug, Serialize, Deserialize)] @@ -18,6 +18,8 @@ pub enum Data { Id(Id), #[serde(skip_deserializing)] Url(Url), + #[serde(skip_deserializing)] + Urn(UrnBuf), #[serde(skip)] Bytes(Vec), #[serde(skip)] @@ -106,6 +108,11 @@ pub enum DataKey { Integer(i64), Number(OrderedFloat), String(Cow<'static, str>), + Id(Id), + #[serde(skip_deserializing)] + Url(Url), + #[serde(skip_deserializing)] + Urn(UrnBuf), } impl DataKey { diff --git a/yazi-shared/src/url/urn.rs b/yazi-shared/src/url/urn.rs index 050ea500..67906407 100644 --- a/yazi-shared/src/url/urn.rs +++ b/yazi-shared/src/url/urn.rs @@ -1,5 +1,7 @@ use std::{borrow::{Borrow, Cow}, ffi::OsStr, ops::Deref, path::{Path, PathBuf}}; +use serde::Serialize; + #[derive(Debug, Eq, PartialEq, Hash)] #[repr(transparent)] pub struct Urn(Path); @@ -48,7 +50,7 @@ impl PartialEq> for &Urn { } // --- UrnBuf -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize)] pub struct UrnBuf(PathBuf); impl Borrow for UrnBuf {