feat: port task manager to Lua (#3134)

This commit is contained in:
三咲雅 misaki masa 2025-09-03 22:25:16 +08:00 committed by GitHub
parent d670da80d3
commit cfc7238638
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 283 additions and 174 deletions

2
Cargo.lock generated
View file

@ -3510,6 +3510,7 @@ dependencies = [
"yazi-parser", "yazi-parser",
"yazi-plugin", "yazi-plugin",
"yazi-proxy", "yazi-proxy",
"yazi-scheduler",
"yazi-shared", "yazi-shared",
"yazi-term", "yazi-term",
"yazi-watcher", "yazi-watcher",
@ -3870,6 +3871,7 @@ dependencies = [
"ordered-float 5.0.0", "ordered-float 5.0.0",
"parking_lot", "parking_lot",
"scopeguard", "scopeguard",
"serde",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",

View file

@ -40,7 +40,7 @@ objc = "0.2.7"
ordered-float = { version = "5.0.0", features = [ "serde" ] } ordered-float = { version = "5.0.0", features = [ "serde" ] }
parking_lot = "0.12.4" parking_lot = "0.12.4"
paste = "1.0.15" paste = "1.0.15"
ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] } ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] }
regex = "1.11.2" regex = "1.11.2"
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.219", features = [ "derive" ] } serde = { version = "1.0.219", features = [ "derive" ] }

View file

@ -13,20 +13,21 @@ default = [ "vendored-lua" ]
vendored-lua = [ "mlua/vendored" ] vendored-lua = [ "mlua/vendored" ]
[dependencies] [dependencies]
yazi-binding = { path = "../yazi-binding", version = "25.6.11" } yazi-binding = { path = "../yazi-binding", version = "25.6.11" }
yazi-boot = { path = "../yazi-boot", version = "25.6.11" } yazi-boot = { path = "../yazi-boot", version = "25.6.11" }
yazi-config = { path = "../yazi-config", version = "25.6.11" } yazi-config = { path = "../yazi-config", version = "25.6.11" }
yazi-core = { path = "../yazi-core", version = "25.6.11" } yazi-core = { path = "../yazi-core", version = "25.6.11" }
yazi-dds = { path = "../yazi-dds", version = "25.6.11" } yazi-dds = { path = "../yazi-dds", version = "25.6.11" }
yazi-fs = { path = "../yazi-fs", version = "25.6.11" } yazi-fs = { path = "../yazi-fs", version = "25.6.11" }
yazi-macro = { path = "../yazi-macro", version = "25.6.11" } yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
yazi-parser = { path = "../yazi-parser", version = "25.6.11" } yazi-parser = { path = "../yazi-parser", version = "25.6.11" }
yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" } yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" } yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" }
yazi-term = { path = "../yazi-term", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" } yazi-term = { path = "../yazi-term", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" } yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }

View file

@ -1,3 +1,3 @@
#![allow(clippy::module_inception)] #![allow(clippy::module_inception)]
yazi_macro::mod_flat!(core file files filter finder folder lives mode preference preview ptr selected tab tabs tasks yanked); yazi_macro::mod_flat!(core file files filter finder folder lives mode preference preview ptr selected tab tabs task tasks yanked);

View file

@ -0,0 +1,37 @@
use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value};
use yazi_binding::cached_field;
use yazi_plugin::runtime::SER_OPT;
use super::{Lives, PtrCell};
pub(super) struct TaskSnap {
inner: PtrCell<yazi_scheduler::TaskSnap>,
v_name: Option<Value>,
v_prog: Option<Value>,
}
impl Deref for TaskSnap {
type Target = yazi_scheduler::TaskSnap;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl TaskSnap {
pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_name: None, v_prog: None })
}
}
impl UserData for TaskSnap {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, name, |lua, me| lua.create_string(&me.name));
cached_field!(fields, prog, |lua, me| lua.to_value_with(&me.prog, SER_OPT));
fields.add_field_method_get("running", |_, me| Ok(me.prog.running()));
fields.add_field_method_get("success", |_, me| Ok(me.prog.success()));
fields.add_field_method_get("percent", |_, me| Ok(me.prog.percent()));
}
}

View file

@ -5,10 +5,12 @@ use yazi_binding::{cached_field, deprecate};
use yazi_plugin::runtime::SER_OPT; use yazi_plugin::runtime::SER_OPT;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
use crate::lives::TaskSnap;
pub(super) struct Tasks { pub(super) struct Tasks {
inner: PtrCell<yazi_core::tasks::Tasks>, inner: PtrCell<yazi_core::tasks::Tasks>,
v_snaps: Option<Value>,
v_summary: Option<Value>, v_summary: Option<Value>,
v_progress: Option<Value>, v_progress: Option<Value>,
} }
@ -21,12 +23,28 @@ impl Deref for Tasks {
impl Tasks { impl Tasks {
pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_summary: None, v_progress: None }) Lives::scoped_userdata(Self {
inner: inner.into(),
v_snaps: None,
v_summary: None,
v_progress: None,
})
} }
} }
impl UserData for Tasks { impl UserData for Tasks {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
cached_field!(fields, snaps, |lua, me| {
let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?;
for snap in &me.snaps {
tbl.raw_push(TaskSnap::make(snap)?)?;
}
Ok(tbl)
});
cached_field!(fields, summary, |lua, me| lua.to_value_with(&me.summary, SER_OPT)); cached_field!(fields, summary, |lua, me| lua.to_value_with(&me.summary, SER_OPT));
cached_field!(fields, progress, |lua, me| { cached_field!(fields, progress, |lua, me| {

View file

@ -47,7 +47,6 @@ yazi_macro::mod_flat!(
update_paged update_paged
update_peeked update_peeked
update_spotted update_spotted
update_tasks
update_yanked update_yanked
visual_mode visual_mode
watch watch

View file

@ -1 +1 @@
yazi_macro::mod_flat!(arrow cancel close inspect open_with process_exec show); yazi_macro::mod_flat!(arrow cancel close inspect open_with process_exec show update_succeed);

View file

@ -1,16 +1,16 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::mgr::UpdateTasksOpt; use yazi_parser::tasks::UpdateSucceedOpt;
use yazi_shared::event::Data; use yazi_shared::event::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct UpdateTasks; pub struct UpdateSucceed;
impl Actor for UpdateTasks { impl Actor for UpdateSucceed {
type Options = UpdateTasksOpt; type Options = UpdateSucceedOpt;
const NAME: &str = "update_tasks"; const NAME: &str = "update_succeed";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
cx.mgr.watcher.push_files(opt.urls); cx.mgr.watcher.push_files(opt.urls);

View file

@ -25,8 +25,9 @@ pub struct Border {
impl Border { impl Border {
pub fn compose(lua: &Lua) -> mlua::Result<Value> { pub fn compose(lua: &Lua) -> mlua::Result<Value> {
let new = lua let new = lua.create_function(|_, (_, edge): (Table, Edge)| {
.create_function(|_, (_, edge): (Table, Edge)| Ok(Border { edge, ..Default::default() }))?; Ok(Border { edge, r#type: ratatui::widgets::BorderType::Rounded, ..Default::default() })
})?;
let border = lua.create_table_from([ let border = lua.create_table_from([
// Type // Type

View file

@ -100,7 +100,7 @@ perm_exec = { fg = "cyan" }
# Progress # Progress
progress_label = { bold = true } progress_label = { bold = true }
progress_normal = { fg = "blue", bg = "black" } progress_normal = { fg = "blue", bg = "black" }
progress_error = { fg = "red", bg = "black" } progress_error = { fg = "blue", bg = "red" }
# : }}} # : }}}

View file

@ -100,7 +100,7 @@ perm_exec = { fg = "cyan" }
# Progress # Progress
progress_label = { bold = true } progress_label = { bold = true }
progress_normal = { fg = "blue", bg = "black" } progress_normal = { fg = "blue", bg = "black" }
progress_error = { fg = "red", bg = "black" } progress_error = { fg = "blue", bg = "red" }
# : }}} # : }}}

View file

@ -12,5 +12,5 @@ impl Layout {
Self { current: Rect::ZERO, preview: Rect::ZERO, progress: Rect::ZERO } Self { current: Rect::ZERO, preview: Rect::ZERO, progress: Rect::ZERO }
} }
pub const fn limit(&self) -> usize { self.current.height as _ } pub const fn folder_limit(self) -> usize { self.current.height as _ }
} }

View file

@ -130,7 +130,7 @@ impl Folder {
} }
pub fn sync_page(&mut self, force: bool) { pub fn sync_page(&mut self, force: bool) {
let limit = LAYOUT.get().limit(); let limit = LAYOUT.get().folder_limit();
if limit == 0 { if limit == 0 {
return; return;
} }
@ -145,7 +145,7 @@ impl Folder {
let old = self.offset; let old = self.offset;
let len = self.files.len(); let len = self.files.len();
let limit = LAYOUT.get().limit(); let limit = LAYOUT.get().folder_limit();
let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff.get() as usize); let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff.get() as usize);
self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) { self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) {
@ -167,7 +167,7 @@ impl Folder {
pub fn paginate(&self, page: usize) -> &[File] { pub fn paginate(&self, page: usize) -> &[File] {
let len = self.files.len(); let len = self.files.len();
let limit = LAYOUT.get().limit(); let limit = LAYOUT.get().folder_limit();
let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1)); let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1));
let end = ((page + 2) * limit).min(len); let end = ((page + 2) * limit).min(len);
@ -178,7 +178,7 @@ impl Folder {
impl Scrollable for Folder { impl Scrollable for Folder {
fn total(&self) -> usize { self.files.len() } fn total(&self) -> usize { self.files.len() }
fn limit(&self) -> usize { LAYOUT.get().limit() } fn limit(&self) -> usize { LAYOUT.get().folder_limit() }
fn scrolloff(&self) -> usize { (self.limit() / 2).min(YAZI.mgr.scrolloff.get() as usize) } fn scrolloff(&self) -> usize { (self.limit() / 2).min(YAZI.mgr.scrolloff.get() as usize) }

View file

@ -32,7 +32,7 @@ impl Tasks {
let new = ongoing.lock().summary(); let new = ongoing.lock().summary();
if last != new { if last != new {
last = new; last = new;
AppProxy::update_summary(new); AppProxy::update_progress(new);
} }
} }
}); });

View file

@ -66,7 +66,6 @@ pub enum Spark<'a> {
UpdatePaged(yazi_parser::mgr::UpdatePagedOpt), UpdatePaged(yazi_parser::mgr::UpdatePagedOpt),
UpdatePeeked(yazi_parser::mgr::UpdatePeekedOpt), UpdatePeeked(yazi_parser::mgr::UpdatePeekedOpt),
UpdateSpotted(yazi_parser::mgr::UpdateSpottedOpt), UpdateSpotted(yazi_parser::mgr::UpdateSpottedOpt),
UpdateTasks(yazi_parser::mgr::UpdateTasksOpt),
UpdateYanked(yazi_parser::mgr::UpdateYankedOpt<'a>), UpdateYanked(yazi_parser::mgr::UpdateYankedOpt<'a>),
VisualMode(yazi_parser::mgr::VisualModeOpt), VisualMode(yazi_parser::mgr::VisualModeOpt),
Watch(yazi_parser::VoidOpt), Watch(yazi_parser::VoidOpt),
@ -109,6 +108,7 @@ pub enum Spark<'a> {
// Tasks // Tasks
TasksProcessExec(yazi_parser::tasks::ProcessExecOpt), TasksProcessExec(yazi_parser::tasks::ProcessExecOpt),
TasksUpdateSucceed(yazi_parser::tasks::UpdateSucceedOpt),
// Which // Which
WhichCallback(yazi_parser::which::CallbackOpt), WhichCallback(yazi_parser::which::CallbackOpt),
@ -188,7 +188,6 @@ impl<'a> IntoLua for Spark<'a> {
Self::UpdatePaged(b) => b.into_lua(lua), Self::UpdatePaged(b) => b.into_lua(lua),
Self::UpdatePeeked(b) => b.into_lua(lua), Self::UpdatePeeked(b) => b.into_lua(lua),
Self::UpdateSpotted(b) => b.into_lua(lua), Self::UpdateSpotted(b) => b.into_lua(lua),
Self::UpdateTasks(b) => b.into_lua(lua),
Self::UpdateYanked(b) => b.into_lua(lua), Self::UpdateYanked(b) => b.into_lua(lua),
Self::VisualMode(b) => b.into_lua(lua), Self::VisualMode(b) => b.into_lua(lua),
Self::Watch(b) => b.into_lua(lua), Self::Watch(b) => b.into_lua(lua),
@ -231,6 +230,7 @@ impl<'a> IntoLua for Spark<'a> {
// Tasks // Tasks
Self::TasksProcessExec(b) => b.into_lua(lua), Self::TasksProcessExec(b) => b.into_lua(lua),
Self::TasksUpdateSucceed(b) => b.into_lua(lua),
// Which // Which
Self::WhichCallback(b) => b.into_lua(lua), Self::WhichCallback(b) => b.into_lua(lua),
@ -314,7 +314,6 @@ try_from_spark!(mgr::UpdateMimesOpt, mgr:update_mimes);
try_from_spark!(mgr::UpdatePagedOpt, mgr:update_paged); try_from_spark!(mgr::UpdatePagedOpt, mgr:update_paged);
try_from_spark!(mgr::UpdatePeekedOpt, mgr:update_peeked); try_from_spark!(mgr::UpdatePeekedOpt, mgr:update_peeked);
try_from_spark!(mgr::UpdateSpottedOpt, mgr:update_spotted); try_from_spark!(mgr::UpdateSpottedOpt, mgr:update_spotted);
try_from_spark!(mgr::UpdateTasksOpt, mgr:update_tasks);
try_from_spark!(mgr::UpdateYankedOpt<'a>, mgr:update_yanked); try_from_spark!(mgr::UpdateYankedOpt<'a>, mgr:update_yanked);
try_from_spark!(mgr::VisualModeOpt, mgr:visual_mode); try_from_spark!(mgr::VisualModeOpt, mgr:visual_mode);
try_from_spark!(mgr::YankOpt, mgr:yank); try_from_spark!(mgr::YankOpt, mgr:yank);
@ -323,5 +322,6 @@ try_from_spark!(pick::CloseOpt, pick:close);
try_from_spark!(pick::ShowOpt, pick:show); try_from_spark!(pick::ShowOpt, pick:show);
try_from_spark!(spot::CopyOpt, spot:copy); try_from_spark!(spot::CopyOpt, spot:copy);
try_from_spark!(tasks::ProcessExecOpt, tasks:process_exec); try_from_spark!(tasks::ProcessExecOpt, tasks:process_exec);
try_from_spark!(tasks::UpdateSucceedOpt, tasks:update_succeed);
try_from_spark!(which::CallbackOpt, which:callback); try_from_spark!(which::CallbackOpt, which:callback);
try_from_spark!(which::ShowOpt, which:show); try_from_spark!(which::ShowOpt, which:show);

View file

@ -12,5 +12,5 @@ yazi_macro::mod_flat!(
resume resume
stop stop
update_notify update_notify
update_summary update_progress
); );

View file

@ -1,13 +1,13 @@
use anyhow::Result; use anyhow::Result;
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::app::UpdateSummaryOpt; use yazi_parser::app::UpdateProgressOpt;
use yazi_shared::event::Data; use yazi_shared::event::Data;
use crate::app::App; use crate::app::App;
impl App { impl App {
pub(crate) fn update_summary(&mut self, opt: UpdateSummaryOpt) -> Result<Data> { pub(crate) fn update_progress(&mut self, opt: UpdateProgressOpt) -> Result<Data> {
// Update the progress of all tasks. // Update the progress of all tasks.
let tasks = &mut self.core.tasks; let tasks = &mut self.core.tasks;
let progressed = tasks.summary != opt.summary; let progressed = tasks.summary != opt.summary;

View file

@ -44,7 +44,7 @@ impl<'a> Executor<'a> {
on!(plugin); on!(plugin);
on!(plugin_do); on!(plugin_do);
on!(update_notify); on!(update_notify);
on!(update_summary); on!(update_progress);
on!(resize); on!(resize);
on!(stop); on!(stop);
on!(resume); on!(resume);
@ -65,7 +65,6 @@ impl<'a> Executor<'a> {
} }
on!(cd); on!(cd);
on!(update_tasks);
on!(update_yanked); on!(update_yanked);
on!(update_files); on!(update_files);
@ -155,6 +154,8 @@ impl<'a> Executor<'a> {
}; };
} }
on!(update_succeed);
on!(show); on!(show);
on!(close); on!(close);
on!(arrow); on!(arrow);

30
yazi-fm/src/tasks/list.rs Normal file
View file

@ -0,0 +1,30 @@
use mlua::{ObjectLike, Table};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use tracing::error;
use yazi_binding::elements::render_once;
use yazi_core::Core;
use yazi_plugin::LUA;
pub(crate) struct List<'a> {
core: &'a Core,
}
impl<'a> List<'a> {
#[inline]
pub(crate) fn new(core: &'a Core) -> Self { Self { core } }
}
impl Widget for List<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut f = || {
let area = yazi_binding::elements::Rect::from(area);
let root = LUA.globals().raw_get::<Table>("Tasks")?.call_method::<Table>("new", area)?;
render_once(root.call_method("redraw", ())?, buf, |p| self.core.mgr.area(p));
Ok::<_, mlua::Error>(())
};
if let Err(e) = f() {
error!("Failed to redraw the `Tasks` component:\n{e}");
}
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(progress tasks); yazi_macro::mod_flat!(list progress tasks);

View file

@ -1,7 +1,9 @@
use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Rect}, text::{Line, Text}, widgets::{Block, BorderType, List, Padding, Widget}}; use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Rect}, text::Line, widgets::{Block, BorderType, Widget, WidgetRef}};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_core::{Core, tasks::TASKS_PERCENT}; use yazi_core::{Core, tasks::TASKS_PERCENT};
use crate::tasks::List;
pub(crate) struct Tasks<'a> { pub(crate) struct Tasks<'a> {
core: &'a Core, core: &'a Core,
} }
@ -31,26 +33,14 @@ impl Widget for Tasks<'_> {
let area = Self::area(area); let area = Self::area(area);
yazi_binding::elements::Clear::default().render(area, buf); yazi_binding::elements::Clear::default().render(area, buf);
let block = Block::bordered() let block = Block::bordered()
.title(Line::styled("Tasks", THEME.tasks.title)) .title(Line::styled("Tasks", THEME.tasks.title))
.title_alignment(Alignment::Center) .title_alignment(Alignment::Center)
.padding(Padding::symmetric(1, 1))
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(THEME.tasks.border); .border_style(THEME.tasks.border);
block.render_ref(area, buf);
let inner = block.inner(area); List::new(self.core).render(block.inner(area), buf);
block.render(area, buf);
let tasks = &self.core.tasks;
let items = tasks.snaps.iter().take(inner.height as usize).enumerate().map(|(i, v)| {
let mut item =
Text::from_iter(textwrap::wrap(&v.name, inner.width as usize).into_iter().map(Line::from));
if i == tasks.cursor {
item = item.style(THEME.tasks.hovered);
}
item
});
List::new(items).render(inner, buf);
} }
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(deprecate mouse notify plugin stop update_summary); yazi_macro::mod_flat!(deprecate mouse notify plugin stop update_progress);

View file

@ -3,11 +3,11 @@ use ordered_float::OrderedFloat;
use serde::Serialize; use serde::Serialize;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
pub struct UpdateSummaryOpt { pub struct UpdateProgressOpt {
pub summary: TaskSummary, pub summary: TaskSummary,
} }
impl TryFrom<CmdCow> for UpdateSummaryOpt { impl TryFrom<CmdCow> for UpdateProgressOpt {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> { fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {

View file

@ -37,7 +37,6 @@ yazi_macro::mod_flat!(
update_paged update_paged
update_peeked update_peeked
update_spotted update_spotted
update_tasks
update_yanked update_yanked
visual_mode visual_mode
yank yank

View file

@ -1 +1 @@
yazi_macro::mod_flat!(process_exec); yazi_macro::mod_flat!(process_exec update_succeed);

View file

@ -3,26 +3,26 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::CmdCow, url::UrlBuf}; use yazi_shared::{event::CmdCow, url::UrlBuf};
#[derive(Debug)] #[derive(Debug)]
pub struct UpdateTasksOpt { pub struct UpdateSucceedOpt {
pub urls: Vec<UrlBuf>, pub urls: Vec<UrlBuf>,
} }
impl TryFrom<CmdCow> for UpdateTasksOpt { impl TryFrom<CmdCow> for UpdateSucceedOpt {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> { fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
let Some(urls) = c.take_any("urls") else { let Some(urls) = c.take_any("urls") else {
bail!("Invalid 'urls' argument in UpdateTasksOpt"); bail!("Invalid 'urls' argument in UpdateSucceedOpt");
}; };
Ok(Self { urls }) Ok(Self { urls })
} }
} }
impl FromLua for UpdateTasksOpt { impl FromLua for UpdateSucceedOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) } fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
} }
impl IntoLua for UpdateTasksOpt { impl IntoLua for UpdateSucceedOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) } fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
} }

View file

@ -38,7 +38,7 @@ function Progress:redraw()
if percent then if percent then
label = string.format("%3d%%, ", math.floor(percent)) label = string.format("%3d%%, ", math.floor(percent))
else else
percent = 99 percent = 0
end end
label = label .. string.format("%d left", summary.total - summary.success) label = label .. string.format("%d left", summary.total - summary.success)

View file

@ -0,0 +1,22 @@
Tasks = {
_id = "tasks",
}
function Tasks:new(area) return setmetatable({ _area = area }, { __index = self }) end
function Tasks:reflow() return { self } end
function Tasks:redraw()
local rows = {}
for _, snap in ipairs(cx.tasks.snaps) do
rows[#rows + 1] = ui.Row { snap.name }
end
local tbl = ui.Table(rows)
:area(self._area:pad(ui.Pad.x(1)))
:row(cx.tasks.cursor)
:row_style(th.tasks.hovered)
:widths { ui.Constraint.Fill(1) }
return { tbl }
end

View file

@ -52,6 +52,7 @@ fn stage_1(lua: &'static Lua) -> Result<()> {
lua.load(preset!("components/status")).set_name("status.lua").exec()?; lua.load(preset!("components/status")).set_name("status.lua").exec()?;
lua.load(preset!("components/tab")).set_name("tab.lua").exec()?; lua.load(preset!("components/tab")).set_name("tab.lua").exec()?;
lua.load(preset!("components/tabs")).set_name("tabs.lua").exec()?; lua.load(preset!("components/tabs")).set_name("tabs.lua").exec()?;
lua.load(preset!("components/tasks")).set_name("tasks.lua").exec()?;
Ok(()) Ok(())
} }

View file

@ -51,7 +51,7 @@ impl AppProxy {
emit!(Call(relay!(app:plugin_do).with_any("opt", opt))); emit!(Call(relay!(app:plugin_do).with_any("opt", opt)));
} }
pub fn update_summary(summary: TaskSummary) { pub fn update_progress(summary: TaskSummary) {
emit!(Call(relay!(app:update_summary).with_any("summary", summary))); emit!(Call(relay!(app:update_progress).with_any("summary", summary)));
} }
} }

View file

@ -54,10 +54,6 @@ impl MgrProxy {
emit!(Call(relay!(mgr:update_spotted).with_any("opt", opt))); emit!(Call(relay!(mgr:update_spotted).with_any("opt", opt)));
} }
pub fn update_tasks(url: &UrlBuf) {
emit!(Call(relay!(mgr:update_tasks).with_any("urls", vec![url.clone()])));
}
pub fn update_paged_by(page: usize, only_if: &UrlBuf) { pub fn update_paged_by(page: usize, only_if: &UrlBuf) {
emit!(Call(relay!(mgr:update_paged, [page]).with_any("only-if", only_if.clone()))); emit!(Call(relay!(mgr:update_paged, [page]).with_any("only-if", only_if.clone())));
} }

View file

@ -23,4 +23,8 @@ impl TasksProxy {
}))); })));
rx.await.ok(); rx.await.ok();
} }
pub fn update_succeed(url: impl Into<UrlBuf>) {
emit!(Call(relay!(tasks:update_succeed).with_any("urls", vec![url.into()])));
}
} }

View file

@ -28,6 +28,7 @@ mlua = { workspace = true }
ordered-float = { workspace = true } ordered-float = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
scopeguard = { workspace = true } scopeguard = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-util = { workspace = true } tokio-util = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -1,14 +1,15 @@
use serde::Serialize;
use yazi_parser::app::TaskSummary; use yazi_parser::app::TaskSummary;
// --- Paste // --- Paste
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct FileProgPaste { pub struct FileProgPaste {
pub(crate) total_files: u32, pub total_files: u32,
pub(crate) success_files: u32, pub success_files: u32,
pub(crate) failed_files: u32, pub failed_files: u32,
pub(crate) total_bytes: u64, pub total_bytes: u64,
pub(crate) processed_bytes: u64, pub processed_bytes: u64,
pub(crate) collected: bool, pub collected: bool,
} }
impl From<FileProgPaste> for TaskSummary { impl From<FileProgPaste> for TaskSummary {
@ -23,13 +24,13 @@ impl From<FileProgPaste> for TaskSummary {
} }
impl FileProgPaste { impl FileProgPaste {
pub(crate) fn running(self) -> bool { pub fn running(self) -> bool {
!self.collected || self.success_files + self.failed_files != self.total_files !self.collected || self.success_files + self.failed_files != self.total_files
} }
pub(crate) fn success(self) -> bool { self.collected && self.success_files == self.total_files } pub fn success(self) -> bool { self.collected && self.success_files == self.total_files }
pub(crate) fn percent(self) -> Option<f32> { pub fn percent(self) -> Option<f32> {
Some(if self.success() { Some(if self.success() {
100.0 100.0
} else if self.total_bytes == 0 { } else if self.total_bytes == 0 {
@ -41,9 +42,9 @@ impl FileProgPaste {
} }
// --- Link // --- Link
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct FileProgLink { pub struct FileProgLink {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<FileProgLink> for TaskSummary { impl From<FileProgLink> for TaskSummary {
@ -58,20 +59,20 @@ impl From<FileProgLink> for TaskSummary {
} }
impl FileProgLink { impl FileProgLink {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }
// --- Hardlink // --- Hardlink
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct FileProgHardlink { pub struct FileProgHardlink {
pub(crate) total: u32, pub total: u32,
pub(crate) success: u32, pub success: u32,
pub(crate) failed: u32, pub failed: u32,
pub(crate) collected: bool, pub collected: bool,
} }
impl From<FileProgHardlink> for TaskSummary { impl From<FileProgHardlink> for TaskSummary {
@ -86,24 +87,22 @@ impl From<FileProgHardlink> for TaskSummary {
} }
impl FileProgHardlink { impl FileProgHardlink {
pub(crate) fn running(self) -> bool { pub fn running(self) -> bool { !self.collected || self.success + self.failed != self.total }
!self.collected || self.success + self.failed != self.total
}
pub(crate) fn success(self) -> bool { self.collected && self.success == self.total } pub fn success(self) -> bool { self.collected && self.success == self.total }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }
// --- Delete // --- Delete
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct FileProgDelete { pub struct FileProgDelete {
pub(crate) total_files: u32, pub total_files: u32,
pub(crate) success_files: u32, pub success_files: u32,
pub(crate) failed_files: u32, pub failed_files: u32,
pub(crate) total_bytes: u64, pub total_bytes: u64,
pub(crate) processed_bytes: u64, pub processed_bytes: u64,
pub(crate) collected: bool, pub collected: bool,
} }
impl From<FileProgDelete> for TaskSummary { impl From<FileProgDelete> for TaskSummary {
@ -118,13 +117,13 @@ impl From<FileProgDelete> for TaskSummary {
} }
impl FileProgDelete { impl FileProgDelete {
pub(crate) fn running(self) -> bool { pub fn running(self) -> bool {
!self.collected || self.success_files + self.failed_files != self.total_files !self.collected || self.success_files + self.failed_files != self.total_files
} }
pub(crate) fn success(self) -> bool { self.collected && self.success_files == self.total_files } pub fn success(self) -> bool { self.collected && self.success_files == self.total_files }
pub(crate) fn percent(self) -> Option<f32> { pub fn percent(self) -> Option<f32> {
Some(if self.success() { Some(if self.success() {
100.0 100.0
} else if self.total_bytes == 0 { } else if self.total_bytes == 0 {
@ -136,9 +135,9 @@ impl FileProgDelete {
} }
// --- Trash // --- Trash
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct FileProgTrash { pub struct FileProgTrash {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<FileProgTrash> for TaskSummary { impl From<FileProgTrash> for TaskSummary {
@ -153,9 +152,9 @@ impl From<FileProgTrash> for TaskSummary {
} }
impl FileProgTrash { impl FileProgTrash {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }

View file

@ -1,9 +1,10 @@
use serde::Serialize;
use yazi_parser::app::TaskSummary; use yazi_parser::app::TaskSummary;
// --- Entry // --- Entry
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct PluginProgEntry { pub struct PluginProgEntry {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<PluginProgEntry> for TaskSummary { impl From<PluginProgEntry> for TaskSummary {
@ -18,9 +19,9 @@ impl From<PluginProgEntry> for TaskSummary {
} }
impl PluginProgEntry { impl PluginProgEntry {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }

View file

@ -1,9 +1,10 @@
use serde::Serialize;
use yazi_parser::app::TaskSummary; use yazi_parser::app::TaskSummary;
// --- Fetch // --- Fetch
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct PreworkProgFetch { pub struct PreworkProgFetch {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<PreworkProgFetch> for TaskSummary { impl From<PreworkProgFetch> for TaskSummary {
@ -18,17 +19,17 @@ impl From<PreworkProgFetch> for TaskSummary {
} }
impl PreworkProgFetch { impl PreworkProgFetch {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }
// --- Load // --- Load
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct PreworkProgLoad { pub struct PreworkProgLoad {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<PreworkProgLoad> for TaskSummary { impl From<PreworkProgLoad> for TaskSummary {
@ -43,17 +44,17 @@ impl From<PreworkProgLoad> for TaskSummary {
} }
impl PreworkProgLoad { impl PreworkProgLoad {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }
// --- Size // --- Size
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct PreworkProgSize { pub struct PreworkProgSize {
pub(crate) done: bool, pub done: bool,
} }
impl From<PreworkProgSize> for TaskSummary { impl From<PreworkProgSize> for TaskSummary {
@ -68,9 +69,9 @@ impl From<PreworkProgSize> for TaskSummary {
} }
impl PreworkProgSize { impl PreworkProgSize {
pub(crate) fn running(self) -> bool { !self.done } pub fn running(self) -> bool { !self.done }
pub(crate) fn success(self) -> bool { self.done } pub fn success(self) -> bool { self.done }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }

View file

@ -1,9 +1,10 @@
use serde::Serialize;
use yazi_parser::app::TaskSummary; use yazi_parser::app::TaskSummary;
// --- Block // --- Block
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct ProcessProgBlock { pub struct ProcessProgBlock {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<ProcessProgBlock> for TaskSummary { impl From<ProcessProgBlock> for TaskSummary {
@ -18,17 +19,17 @@ impl From<ProcessProgBlock> for TaskSummary {
} }
impl ProcessProgBlock { impl ProcessProgBlock {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }
// --- Orphan // --- Orphan
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct ProcessProgOrphan { pub struct ProcessProgOrphan {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<ProcessProgOrphan> for TaskSummary { impl From<ProcessProgOrphan> for TaskSummary {
@ -43,17 +44,17 @@ impl From<ProcessProgOrphan> for TaskSummary {
} }
impl ProcessProgOrphan { impl ProcessProgOrphan {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }
// --- Bg // --- Bg
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub(crate) struct ProcessProgBg { pub struct ProcessProgBg {
pub(crate) state: Option<bool>, pub state: Option<bool>,
} }
impl From<ProcessProgBg> for TaskSummary { impl From<ProcessProgBg> for TaskSummary {
@ -68,9 +69,9 @@ impl From<ProcessProgBg> for TaskSummary {
} }
impl ProcessProgBg { impl ProcessProgBg {
pub(crate) fn running(self) -> bool { self.state.is_none() } pub fn running(self) -> bool { self.state.is_none() }
pub(crate) fn success(self) -> bool { self.state == Some(true) } pub fn success(self) -> bool { self.state == Some(true) }
pub(crate) fn percent(self) -> Option<f32> { None } pub fn percent(self) -> Option<f32> { None }
} }

View file

@ -1,9 +1,11 @@
use serde::Serialize;
use yazi_parser::app::TaskSummary; use yazi_parser::app::TaskSummary;
use crate::{file::{FileProgDelete, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash}, impl_from_prog, plugin::PluginProgEntry, prework::{PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; use crate::{file::{FileProgDelete, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash}, impl_from_prog, plugin::PluginProgEntry, prework::{PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub(crate) enum TaskProg { #[serde(tag = "kind")]
pub enum TaskProg {
// File // File
FilePaste(FileProgPaste), FilePaste(FileProgPaste),
FileLink(FileProgLink), FileLink(FileProgLink),
@ -57,7 +59,7 @@ impl From<TaskProg> for TaskSummary {
} }
impl TaskProg { impl TaskProg {
pub(crate) fn running(self) -> bool { pub fn running(self) -> bool {
match self { match self {
// File // File
Self::FilePaste(p) => p.running(), Self::FilePaste(p) => p.running(),
@ -78,7 +80,7 @@ impl TaskProg {
} }
} }
pub(crate) fn success(self) -> bool { pub fn success(self) -> bool {
match self { match self {
// File // File
Self::FilePaste(p) => p.success(), Self::FilePaste(p) => p.success(),
@ -99,7 +101,7 @@ impl TaskProg {
} }
} }
pub(crate) fn percent(self) -> Option<f32> { pub fn percent(self) -> Option<f32> {
match self { match self {
// File // File
Self::FilePaste(p) => p.percent(), Self::FilePaste(p) => p.percent(),

View file

@ -8,7 +8,7 @@ use yazi_config::{YAZI, plugin::{Fetcher, Preloader}};
use yazi_dds::Pump; use yazi_dds::Pump;
use yazi_fs::{must_be_dir, path::unique_name, provider, remove_dir_clean}; use yazi_fs::{must_be_dir, path::unique_name, provider, remove_dir_clean};
use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt}; use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt};
use yazi_proxy::MgrProxy; use yazi_proxy::TasksProxy;
use yazi_shared::{Id, Throttle, url::UrlBuf}; use yazi_shared::{Id, Throttle, url::UrlBuf};
use super::{Ongoing, TaskOp}; use super::{Ongoing, TaskOp};
@ -174,7 +174,7 @@ impl Scheduler {
move |canceled: bool| async move { move |canceled: bool| async move {
if !canceled { if !canceled {
provider::remove_dir_all(&target).await.ok(); provider::remove_dir_all(&target).await.ok();
MgrProxy::update_tasks(&target); TasksProxy::update_succeed(&target);
Pump::push_delete(target); Pump::push_delete(target);
} }
ongoing.lock().remove(id); ongoing.lock().remove(id);
@ -199,7 +199,7 @@ impl Scheduler {
move |canceled: bool| async move { move |canceled: bool| async move {
if !canceled { if !canceled {
MgrProxy::update_tasks(&target); TasksProxy::update_succeed(&target);
Pump::push_trash(target); Pump::push_trash(target);
} }
ongoing.lock().remove(id); ongoing.lock().remove(id);

View file

@ -1,10 +1,13 @@
use crate::Task; use serde::Serialize;
#[derive(Debug, PartialEq, Eq)] use crate::{Task, TaskProg};
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct TaskSnap { pub struct TaskSnap {
pub name: String, pub name: String,
pub prog: TaskProg,
} }
impl From<&Task> for TaskSnap { impl From<&Task> for TaskSnap {
fn from(task: &Task) -> Self { TaskSnap { name: task.name.clone() } } fn from(task: &Task) -> Self { TaskSnap { name: task.name.clone(), prog: task.prog } }
} }