feat: hover cursor over the new file after extracting (#3854)

This commit is contained in:
sxyazi 2026-04-06 15:32:18 +08:00
parent ec178fdb52
commit 354cdef907
No known key found for this signature in database
52 changed files with 979 additions and 321 deletions

View file

@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Custom tab name ([#3666]) - Custom tab name ([#3666])
- New `--in` for `search` action to set search directory ([#3696]) - New `--in` for `search` action to set search directory ([#3696])
- Hover cursor over the new file after copying/cutting/linking/hardlinking ([#3846]) - Hover cursor over the new file after copying/cutting/linking/hardlinking/extracting ([#3846], [#3854])
- Multi-file spotter ([#3733]) - Multi-file spotter ([#3733])
- Vim-like `lua` action that runs an inline Lua snippet ([#3813]) - Vim-like `lua` action that runs an inline Lua snippet ([#3813])
- Certificate authentication for SFTP VFS provider ([#3716]) - Certificate authentication for SFTP VFS provider ([#3716])
@ -1696,3 +1696,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3804]: https://github.com/sxyazi/yazi/pull/3804 [#3804]: https://github.com/sxyazi/yazi/pull/3804
[#3813]: https://github.com/sxyazi/yazi/pull/3813 [#3813]: https://github.com/sxyazi/yazi/pull/3813
[#3846]: https://github.com/sxyazi/yazi/pull/3846 [#3846]: https://github.com/sxyazi/yazi/pull/3846
[#3854]: https://github.com/sxyazi/yazi/pull/3854

9
Cargo.lock generated
View file

@ -6102,6 +6102,7 @@ dependencies = [
"paste", "paste",
"ratatui", "ratatui",
"serde", "serde",
"serde_with",
"strum 0.28.0", "strum 0.28.0",
"tokio", "tokio",
"yazi-binding", "yazi-binding",
@ -6111,7 +6112,6 @@ dependencies = [
"yazi-dds", "yazi-dds",
"yazi-fs", "yazi-fs",
"yazi-macro", "yazi-macro",
"yazi-runner",
"yazi-scheduler", "yazi-scheduler",
"yazi-shared", "yazi-shared",
"yazi-vfs", "yazi-vfs",
@ -6165,11 +6165,11 @@ checksum = "33232d9116df6415ddfcdf72701b1b7439ce87f240a14723e8dd5d17e7ed5f98"
name = "yazi-proxy" name = "yazi-proxy"
version = "26.2.2" version = "26.2.2"
dependencies = [ dependencies = [
"anyhow",
"tokio", "tokio",
"yazi-config", "yazi-config",
"yazi-core", "yazi-core",
"yazi-macro", "yazi-macro",
"yazi-runner",
"yazi-scheduler", "yazi-scheduler",
"yazi-shared", "yazi-shared",
"yazi-widgets", "yazi-widgets",
@ -6180,12 +6180,9 @@ name = "yazi-runner"
version = "26.2.2" version = "26.2.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dyn-clone",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"mlua", "mlua",
"parking_lot", "parking_lot",
"serde",
"strum 0.28.0",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -6215,7 +6212,6 @@ dependencies = [
"serde", "serde",
"strum 0.28.0", "strum 0.28.0",
"tokio", "tokio",
"tokio-util",
"tracing", "tracing",
"yazi-binding", "yazi-binding",
"yazi-config", "yazi-config",
@ -6360,6 +6356,7 @@ dependencies = [
"mlua", "mlua",
"parking_lot", "parking_lot",
"ratatui", "ratatui",
"serde",
"tokio", "tokio",
"unicode-width", "unicode-width",
"yazi-adapter", "yazi-adapter",

View file

@ -24,7 +24,7 @@ impl Actor for Plugin {
} }
if opt.mode == PluginMode::Async { if opt.mode == PluginMode::Async {
succ!(cx.core.tasks.scheduler.plugin_entry(opt.id, opt.args)); succ!(cx.core.tasks.scheduler.plugin_entry(opt.into()));
} else if opt.mode == PluginMode::Sync && hits { } else if opt.mode == PluginMode::Sync && hits {
return act!(app:plugin_do, cx, opt); return act!(app:plugin_do, cx, opt);
} }

View file

@ -31,7 +31,7 @@ impl Actor for PluginDo {
} }
if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync { if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync {
succ!(cx.core.tasks.scheduler.plugin_entry(opt.id, opt.args)); succ!(cx.core.tasks.scheduler.plugin_entry(opt.into()));
} }
let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true); let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true);

View file

@ -9,8 +9,8 @@ use super::{Lives, PtrCell};
pub(super) struct TaskSnap { pub(super) struct TaskSnap {
inner: PtrCell<yazi_scheduler::TaskSnap>, inner: PtrCell<yazi_scheduler::TaskSnap>,
v_name: Option<Value>, v_title: Option<Value>,
v_prog: Option<Value>, v_prog: Option<Value>,
} }
impl Deref for TaskSnap { impl Deref for TaskSnap {
@ -21,13 +21,13 @@ impl Deref for TaskSnap {
impl TaskSnap { impl TaskSnap {
pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_name: None, v_prog: None }) Lives::scoped_userdata(Self { inner: inner.into(), v_title: None, v_prog: None })
} }
} }
impl UserData for TaskSnap { impl UserData for TaskSnap {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, name, |lua, me| lua.create_string(&me.name)); cached_field!(fields, title, |lua, me| lua.create_string(&me.title));
cached_field!(fields, prog, |lua, me| lua.to_value_with(&me.prog, SER_OPT)); 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("running", |_, me| Ok(me.prog.running()));

View file

@ -20,9 +20,9 @@ impl Actor for Quit {
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let ongoing = cx.tasks.scheduler.ongoing.clone(); let ongoing = cx.tasks.scheduler.ongoing.clone();
let (left, left_names) = { let (left, left_titles) = {
let ongoing = ongoing.lock(); let ongoing = ongoing.lock();
(ongoing.len(), ongoing.values().take(11).map(|t| t.name.clone()).collect()) (ongoing.len(), ongoing.values().take(11).map(|t| t.title.clone()).collect())
}; };
if left == 0 { if left == 0 {
@ -31,7 +31,7 @@ impl Actor for Quit {
tokio::spawn(async move { tokio::spawn(async move {
let mut i = 0; let mut i = 0;
let token = ConfirmProxy::show_sync(ConfirmCfg::quit(left, left_names)); let token = ConfirmProxy::show_sync(ConfirmCfg::quit(left, left_titles));
loop { loop {
select! { select! {
_ = time::sleep(Duration::from_millis(50)) => { _ = time::sleep(Duration::from_millis(50)) => {

View file

@ -1 +1 @@
yazi_macro::mod_flat!(arrow cancel close open_shell_compat inspect process_open show update_succeed); yazi_macro::mod_flat!(arrow cancel close inspect open_shell_compat process_open show spawn update_succeed);

View file

@ -0,0 +1,21 @@
use anyhow::Result;
use yazi_core::tasks::TaskOpt;
use yazi_macro::succ;
use yazi_parser::tasks::SpawnForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Spawn;
impl Actor for Spawn {
type Form = SpawnForm;
const NAME: &str = "spawn";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
succ!(match form.opt {
TaskOpt::Plugin(r#in) => cx.tasks.scheduler.plugin_entry(r#in),
})
}
}

View file

@ -6,6 +6,7 @@ use hashbrown::HashMap;
use mlua::{Lua, Table}; use mlua::{Lua, Table};
use serde::Deserialize; use serde::Deserialize;
use strum::EnumString; use strum::EnumString;
use yazi_scheduler::plugin::PluginInEntry;
use yazi_shared::{SStr, data::{Data, DataKey}, event::{Action, ActionCow}}; use yazi_shared::{SStr, data::{Data, DataKey}, event::{Action, ActionCow}};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -36,6 +37,12 @@ impl TryFrom<ActionCow> for PluginOpt {
} }
} }
impl From<PluginOpt> for PluginInEntry {
fn from(value: PluginOpt) -> Self {
Self { plugin: value.id, args: value.args, ..Default::default() }
}
}
impl PluginOpt { impl PluginOpt {
pub fn new_callback(id: impl Into<SStr>, f: impl PluginCallback) -> Self { pub fn new_callback(id: impl Into<SStr>, f: impl PluginCallback) -> Self {
Self { Self {

View file

@ -1,4 +1,4 @@
yazi_macro::mod_flat!(file prework process tasks); yazi_macro::mod_flat!(file option prework process tasks);
pub const TASKS_BORDER: u16 = 2; pub const TASKS_BORDER: u16 = 2;
pub const TASKS_PADDING: u16 = 2; pub const TASKS_PADDING: u16 = 2;

View file

@ -0,0 +1,6 @@
use yazi_scheduler::plugin::PluginInEntry;
#[derive(Clone, Debug)]
pub enum TaskOpt {
Plugin(PluginInEntry),
}

View file

@ -175,6 +175,7 @@ impl<'a> Executor<'a> {
} }
on!(update_succeed); on!(update_succeed);
on!(spawn);
on!(show); on!(show);
on!(close); on!(close);

View file

@ -24,23 +24,23 @@ yazi-core = { path = "../yazi-core", version = "26.2.2" }
yazi-dds = { path = "../yazi-dds", version = "26.2.2" } yazi-dds = { path = "../yazi-dds", version = "26.2.2" }
yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" }
yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" }
yazi-runner = { path = "../yazi-runner", version = "26.2.2" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.2.2" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.2.2" }
yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" }
yazi-vfs = { path = "../yazi-vfs", version = "26.2.2" } yazi-vfs = { path = "../yazi-vfs", version = "26.2.2" }
yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" } yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
strum = { workspace = true } serde_with = { workspace = true }
tokio = { workspace = true } strum = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,23 +1,18 @@
use anyhow::bail;
use mlua::{ExternalError, IntoLua, Lua, Value}; use mlua::{ExternalError, IntoLua, Lua, Value};
use serde::Deserialize;
use yazi_shared::event::ActionCow; use yazi_shared::event::ActionCow;
use yazi_widgets::Step; use yazi_widgets::Step;
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct ArrowForm { pub struct ArrowForm {
#[serde(alias = "0")]
pub step: Step, pub step: Step,
} }
impl TryFrom<ActionCow> for ArrowForm { impl TryFrom<ActionCow> for ArrowForm {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(a: ActionCow) -> Result<Self, Self::Error> { fn try_from(a: ActionCow) -> Result<Self, Self::Error> { Ok(a.deserialize()?) }
let Ok(step) = a.first() else {
bail!("Invalid 'step' in ArrowForm");
};
Ok(Self { step })
}
} }
impl From<isize> for ArrowForm { impl From<isize> for ArrowForm {

View file

@ -1,5 +1,3 @@
use std::str::FromStr;
use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value}; use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yazi_binding::SER_OPT; use yazi_binding::SER_OPT;
@ -7,15 +5,14 @@ use yazi_shared::event::ActionCow;
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
pub struct HiddenForm { pub struct HiddenForm {
#[serde(alias = "0")]
pub state: HiddenFormState, pub state: HiddenFormState,
} }
impl TryFrom<ActionCow> for HiddenForm { impl TryFrom<ActionCow> for HiddenForm {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(a: ActionCow) -> Result<Self, Self::Error> { fn try_from(a: ActionCow) -> Result<Self, Self::Error> { Ok(a.deserialize()?) }
Ok(Self { state: a.str(0).parse().unwrap_or_default() })
}
} }
impl FromLua for HiddenForm { impl FromLua for HiddenForm {
@ -37,14 +34,6 @@ pub enum HiddenFormState {
Toggle, Toggle,
} }
impl FromStr for HiddenFormState {
type Err = serde::de::value::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::deserialize(serde::de::value::StrDeserializer::new(s))
}
}
impl HiddenFormState { impl HiddenFormState {
pub fn bool(self, old: bool) -> bool { pub fn bool(self, old: bool) -> bool {
match self { match self {

View file

@ -1,28 +1,22 @@
use std::time::Duration; use std::time::Duration;
use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::Deserialize;
use serde_with::{DurationSecondsWithFrac, serde_as};
use yazi_shared::event::ActionCow; use yazi_shared::event::ActionCow;
#[derive(Debug, Default)] #[serde_as]
#[derive(Debug, Default, Deserialize)]
pub struct TickForm { pub struct TickForm {
#[serde(alias = "0")]
#[serde_as(as = "DurationSecondsWithFrac<f64>")]
pub interval: Duration, pub interval: Duration,
} }
impl TryFrom<ActionCow> for TickForm { impl TryFrom<ActionCow> for TickForm {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(a: ActionCow) -> Result<Self, Self::Error> { fn try_from(a: ActionCow) -> Result<Self, Self::Error> { Ok(a.deserialize()?) }
let Ok(interval) = a.first() else {
bail!("Invalid 'interval' in TickForm");
};
if interval < 0.0 {
bail!("'interval' must be non-negative in TickForm");
}
Ok(Self { interval: Duration::from_secs_f64(interval) })
}
} }
impl FromLua for TickForm { impl FromLua for TickForm {

View file

@ -148,6 +148,7 @@ pub enum Spark<'a> {
TasksOpenShellCompat(crate::tasks::ProcessOpenForm), TasksOpenShellCompat(crate::tasks::ProcessOpenForm),
TasksProcessOpen(crate::tasks::ProcessOpenForm), TasksProcessOpen(crate::tasks::ProcessOpenForm),
TasksShow(crate::VoidForm), TasksShow(crate::VoidForm),
TasksSpawn(crate::tasks::SpawnForm),
TasksUpdateSucceed(crate::tasks::UpdateSucceedForm), TasksUpdateSucceed(crate::tasks::UpdateSucceedForm),
// Which // Which
@ -331,6 +332,7 @@ impl<'a> IntoLua for Spark<'a> {
Self::TasksOpenShellCompat(b) => b.into_lua(lua), Self::TasksOpenShellCompat(b) => b.into_lua(lua),
Self::TasksProcessOpen(b) => b.into_lua(lua), Self::TasksProcessOpen(b) => b.into_lua(lua),
Self::TasksShow(b) => b.into_lua(lua), Self::TasksShow(b) => b.into_lua(lua),
Self::TasksSpawn(b) => b.into_lua(lua),
Self::TasksUpdateSucceed(b) => b.into_lua(lua), Self::TasksUpdateSucceed(b) => b.into_lua(lua),
// Which // Which
@ -433,6 +435,7 @@ try_from_spark!(crate::pick::CloseForm, pick:close);
try_from_spark!(crate::pick::ShowForm, pick:show); try_from_spark!(crate::pick::ShowForm, pick:show);
try_from_spark!(crate::spot::CopyForm, spot:copy); try_from_spark!(crate::spot::CopyForm, spot:copy);
try_from_spark!(crate::tasks::ProcessOpenForm, tasks:process_open); try_from_spark!(crate::tasks::ProcessOpenForm, tasks:process_open);
try_from_spark!(crate::tasks::SpawnForm, tasks:spawn);
try_from_spark!(crate::tasks::UpdateSucceedForm, tasks:update_succeed); try_from_spark!(crate::tasks::UpdateSucceedForm, tasks:update_succeed);
try_from_spark!(crate::which::ActivateForm, which:activate); try_from_spark!(crate::which::ActivateForm, which:activate);
try_from_spark!(yazi_dds::Payload<'a>, app:accept_payload); try_from_spark!(yazi_dds::Payload<'a>, app:accept_payload);

View file

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

View file

@ -0,0 +1,25 @@
use anyhow::anyhow;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_core::tasks::TaskOpt;
use yazi_shared::event::ActionCow;
#[derive(Clone, Debug)]
pub struct SpawnForm {
pub opt: TaskOpt,
}
impl TryFrom<ActionCow> for SpawnForm {
type Error = anyhow::Error;
fn try_from(mut a: ActionCow) -> Result<Self, Self::Error> {
Ok(Self { opt: a.take_any("opt").ok_or_else(|| anyhow!("Invalid 'opt' in SpawnForm"))? })
}
}
impl FromLua for SpawnForm {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for SpawnForm {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -29,7 +29,7 @@ function Tasks:redraw()
break break
end end
elements[#elements + 1] = ui.Line({ self:icon(snap), snap.name }):area(ui.Rect { elements[#elements + 1] = ui.Line({ self:icon(snap), snap.title }):area(ui.Rect {
x = self._area.x, x = self._area.x,
y = y, y = y,
w = self._area.w, w = self._area.w,

View file

@ -4,31 +4,48 @@ local M = {}
function M:setup() function M:setup()
ps.sub_remote("extract", function(args) ps.sub_remote("extract", function(args)
local noisy = #args == 1 and ' "" --noisy' or ' ""' ya.async(function()
for _, arg in ipairs(args) do for i, arg in ipairs(args) do
ya.emit("plugin", { self._id, ya.quote(arg, true) .. noisy }) ya.task("plugin", {
end self._id,
args = { arg, "", noisy = #args == 1 },
title = "Extract " .. arg,
track = i == 1,
}):spawn()
end
end)
end) end)
end end
function M:entry(job) function M:init(job)
local from = job.args[1] and Url(job.args[1]) local from = job.args[1] and Url(job.args[1])
local to = job.args[2] ~= "" and Url(job.args[2]) or nil
if not from then if not from then
fail("No URL provided") fail("No URL provided")
end end
local pwd = "" local to = job.args[2] ~= "" and Url(job.args[2]) or from.parent
if not to then
fail("Failed to determine target directory for '%s'", from)
end
self.job = { id = job.id, from = from, to = to }
end
function M:entry(job)
self:init(job)
local pwd, target, retry = "", nil, false
while true do while true do
if not M:try_with(from, pwd, to) then target, retry = self:try_with(pwd)
if not retry then
break break
elseif not job.args.noisy then elseif not job.args.noisy then
fail("'%s' is password-protected, please extract it individually and enter the password", from) fail("'%s' is password-protected, please extract it individually and enter the password", self.job.from)
end end
local value, event = ya.input { local value, event = ya.input {
pos = { "top-center", y = 2, w = 50 }, pos = { "top-center", y = 2, w = 50 },
title = string.format('Password for "%s":', from.name), title = string.format('Password for "%s":', self.job.from.name),
obscure = true, obscure = true,
} }
if event == 1 then if event == 1 then
@ -37,14 +54,14 @@ function M:entry(job)
break break
end end
end end
if target then
ya.emit("tasks:update_succeed", { job.id, urls = { target }, track = true })
end
end end
function M:try_with(from, pwd, to) function M:try_with(pwd)
to = to or from.parent local from, to = self.job.from, self.job.to
if not to then
fail("Invalid URL '%s'", from)
end
local tmp = fs.unique("dir", to:join(self.tmp_name(from))) local tmp = fs.unique("dir", to:join(self.tmp_name(from)))
if not tmp then if not tmp then
fail("Failed to determine a temporary directory for %s", from) fail("Failed to determine a temporary directory for %s", from)
@ -59,18 +76,21 @@ function M:try_with(from, pwd, to)
local output, err = child:wait_with_output() local output, err = child:wait_with_output()
if output and output.status.code == 2 and archive.is_encrypted(output.stderr) then if output and output.status.code == 2 and archive.is_encrypted(output.stderr) then
fs.remove("dir_all", tmp) fs.remove("dir_all", tmp)
return true -- Need to retry return nil, true -- Need to retry
end end
self:tidy(from, to, tmp) local target = self:tidy(tmp)
if not output then if not output then
fail("7zip failed to output when extracting '%s', error: %s", from, err) fail("7zip failed to output when extracting '%s', error: %s", from, err)
elseif output.status.code ~= 0 then elseif output.status.code ~= 0 then
fail("7zip exited with error code %s when extracting '%s':\n%s", output.status.code, from, output.stderr) fail("7zip exited with error code %s when extracting '%s':\n%s", output.status.code, from, output.stderr)
else
return target, false
end end
end end
function M:tidy(from, to, tmp) function M:tidy(tmp)
local from, to = self.job.from, self.job.to
local outs = fs.read_dir(tmp, { limit = 2 }) local outs = fs.read_dir(tmp, { limit = 2 })
if not outs then if not outs then
fail("Failed to read the temporary directory '%s' when extracting '%s'", tmp, from) fail("Failed to read the temporary directory '%s' when extracting '%s'", tmp, from)
@ -81,7 +101,7 @@ function M:tidy(from, to, tmp)
local only = #outs == 1 and outs[1] local only = #outs == 1 and outs[1]
if only and not only.cha.is_dir and require("archive").is_tar(only.url) then if only and not only.cha.is_dir and require("archive").is_tar(only.url) then
self:entry { args = { tostring(only.url), tostring(to) } } self:entry { id = self.job.id, args = { tostring(only.url), tostring(to) } }
fs.remove("file", only.url) fs.remove("file", only.url)
fs.remove("dir", tmp) fs.remove("dir", tmp)
return return
@ -98,7 +118,9 @@ function M:tidy(from, to, tmp)
elseif not only and not fs.rename(tmp, target) then elseif not only and not fs.rename(tmp, target) then
fail('Failed to move "%s" to "%s"', tmp, target) fail('Failed to move "%s" to "%s"', tmp, target)
end end
fs.remove("dir", tmp) fs.remove("dir", tmp)
return target
end end
function M.tmp_name(url) return ".tmp_" .. ya.hash(string.format("extract//%s//%.10f", url, ya.time())) end function M.tmp_name(url) return ".tmp_" .. ya.hash(string.format("extract//%s//%.10f", url, ya.time())) end

View file

@ -1,4 +1,4 @@
yazi_macro::mod_pub!(elements external fs pubsub runtime theme utils); yazi_macro::mod_pub!(elements external fs pubsub runtime tasks theme utils);
yazi_macro::mod_flat!(slim standard); yazi_macro::mod_flat!(slim standard);

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(option task);

View file

@ -0,0 +1,15 @@
use mlua::{UserData, UserDataMethods};
use yazi_proxy::TasksProxy;
use crate::tasks::Task;
#[derive(Clone, Debug)]
pub(crate) struct TaskOpt(pub(crate) yazi_core::tasks::TaskOpt);
impl UserData for TaskOpt {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method_once("spawn", |_, me, ()| async move {
Ok(Task { id: TasksProxy::spawn(me.0).await? })
});
}
}

View file

@ -0,0 +1,13 @@
use mlua::{UserData, UserDataFields};
use yazi_shared::Id;
#[derive(Clone, Debug)]
pub(crate) struct Task {
pub(super) id: Id,
}
impl UserData for Task {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(yazi_binding::Id(me.id)));
}
}

View file

@ -1,3 +1,3 @@
yazi_macro::mod_flat!( yazi_macro::mod_flat!(
app cache call image json layer log preview process spot sync target text time user utils app cache call image json layer log preview process spot sync target tasks text time user utils
); );

View file

@ -0,0 +1,16 @@
use mlua::{ExternalError, FromLua, Function, Lua, Value};
use yazi_core::tasks;
use super::Utils;
use crate::tasks::TaskOpt;
impl Utils {
pub(super) fn task(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, (kind, value): (mlua::String, Value)| {
Ok(TaskOpt(match &*kind.as_bytes() {
b"plugin" => tasks::TaskOpt::Plugin(<_>::from_lua(value, lua)?),
_ => Err(format!("unsupported spawn kind: {}", kind.display()).into_lua_err())?,
}))
})
}
}

View file

@ -82,6 +82,9 @@ pub fn compose(
#[cfg(unix)] #[cfg(unix)]
b"host_name" => Utils::host_name(lua)?, b"host_name" => Utils::host_name(lua)?,
// Task
b"task" => Utils::task(lua)?,
_ => return Ok(Value::Nil), _ => return Ok(Value::Nil),
} }
.into_lua(lua) .into_lua(lua)

View file

@ -16,10 +16,10 @@ workspace = true
yazi-config = { path = "../yazi-config", version = "26.2.2" } yazi-config = { path = "../yazi-config", version = "26.2.2" }
yazi-core = { path = "../yazi-core", version = "26.2.2" } yazi-core = { path = "../yazi-core", version = "26.2.2" }
yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" }
yazi-runner = { path = "../yazi-runner", version = "26.2.2" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.2.2" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.2.2" }
yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" }
yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" } yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" }
# External dependencies # External dependencies
tokio = { workspace = true } anyhow = { workspace = true }
tokio = { workspace = true }

View file

@ -1,13 +1,22 @@
use std::ffi::OsString; use std::ffi::OsString;
use anyhow::{Result, anyhow};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_core::tasks::TaskOpt;
use yazi_macro::{emit, relay}; use yazi_macro::{emit, relay};
use yazi_scheduler::process::ProcessOpt; use yazi_scheduler::process::ProcessOpt;
use yazi_shared::url::UrlCow; use yazi_shared::{Id, url::UrlCow};
pub struct TasksProxy; pub struct TasksProxy;
impl TasksProxy { impl TasksProxy {
pub async fn spawn(opt: TaskOpt) -> Result<Id> {
let (tx, mut rx) = mpsc::unbounded_channel();
emit!(Call(relay!(tasks:spawn).with_any("opt", opt).with_replier(tx)));
rx.recv().await.ok_or_else(|| anyhow!("channel closed"))??.try_into()
}
// TODO: remove // TODO: remove
pub fn open_shell_compat(opt: ProcessOpt) { pub fn open_shell_compat(opt: ProcessOpt) {
emit!(Call(relay!(tasks:open_shell_compat).with_any("opt", opt))); emit!(Call(relay!(tasks:open_shell_compat).with_any("opt", opt)));

View file

@ -23,12 +23,9 @@ yazi-shared = { path = "../yazi-shared", version = "26.2.2" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
dyn-clone = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
serde = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-util = { workspace = true } tokio-util = { workspace = true }

View file

@ -35,7 +35,6 @@ parking_lot = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]

View file

@ -39,7 +39,7 @@ impl Fetch {
loaded.get_mut(&h).map(|x| *x &= !(1 << plugin.idx)); loaded.get_mut(&h).map(|x| *x &= !(1 << plugin.idx));
} }
if let Some(e) = err { if let Some(e) = err {
error!("Error when running fetcher `{}`:\n{e}", plugin.run.name); error!("Error when running fetcher '{}':\n{e}", plugin.run.name);
} }
Ok(self.ops.out(id, FetchOutFetch::Succ)) Ok(self.ops.out(id, FetchOutFetch::Succ))

View file

@ -1,7 +1,11 @@
use std::borrow::Cow;
use yazi_config::plugin::Fetcher; use yazi_config::plugin::Fetcher;
use yazi_runner::fetcher::FetchJob; use yazi_runner::fetcher::FetchJob;
use yazi_shared::Id; use yazi_shared::Id;
use crate::{TaskIn, fetch::FetchProg};
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct FetchIn { pub(crate) struct FetchIn {
pub(crate) id: Id, pub(crate) id: Id,
@ -9,10 +13,21 @@ pub(crate) struct FetchIn {
pub(crate) targets: Vec<yazi_fs::File>, pub(crate) targets: Vec<yazi_fs::File>,
} }
impl TaskIn for FetchIn {
type Prog = FetchProg;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Run fetcher '{}' with {} target(s)", self.plugin.run.name, self.targets.len()).into()
}
}
impl From<FetchIn> for FetchJob { impl From<FetchIn> for FetchJob {
fn from(value: FetchIn) -> Self { Self { action: &value.plugin.run, files: value.targets } } fn from(value: FetchIn) -> Self { Self { action: &value.plugin.run, files: value.targets } }
} }
impl FetchIn {
pub(crate) fn id(&self) -> Id { self.id }
}

View file

@ -1,8 +1,10 @@
use std::{mem, path::PathBuf}; use std::{borrow::Cow, mem, path::PathBuf};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_fs::cha::Cha; use yazi_fs::cha::Cha;
use yazi_shared::{Id, url::UrlBuf}; use yazi_shared::{Id, url::{UrlBuf, UrlLike}};
use crate::{TaskIn, file::{FileProgCopy, FileProgCut, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgTrash, FileProgUpload}};
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum FileIn { pub(crate) enum FileIn {
@ -24,6 +26,74 @@ pub(crate) enum FileIn {
UploadDo(FileInUpload), UploadDo(FileInUpload),
} }
impl TaskIn for FileIn {
type Prog = ();
fn id(&self) -> Id {
match self {
Self::Copy(r#in) => r#in.id(),
Self::CopyDo(r#in) => r#in.id(),
Self::Cut(r#in) => r#in.id(),
Self::CutDo(r#in) => r#in.id(),
Self::Link(r#in) => r#in.id(),
Self::LinkDo(r#in) => r#in.id(),
Self::Hardlink(r#in) => r#in.id(),
Self::HardlinkDo(r#in) => r#in.id(),
Self::Delete(r#in) => r#in.id(),
Self::DeleteDo(r#in) => r#in.id(),
Self::Trash(r#in) => r#in.id(),
Self::TrashDo(r#in) => r#in.id(),
Self::Download(r#in) => r#in.id(),
Self::DownloadDo(r#in) => r#in.id(),
Self::Upload(r#in) => r#in.id(),
Self::UploadDo(r#in) => r#in.id(),
}
}
fn with_id(&mut self, id: Id) -> &mut Self {
match self {
Self::Copy(r#in) => _ = r#in.with_id(id),
Self::CopyDo(r#in) => _ = r#in.with_id(id),
Self::Cut(r#in) => _ = r#in.with_id(id),
Self::CutDo(r#in) => _ = r#in.with_id(id),
Self::Link(r#in) => _ = r#in.with_id(id),
Self::LinkDo(r#in) => _ = r#in.with_id(id),
Self::Hardlink(r#in) => _ = r#in.with_id(id),
Self::HardlinkDo(r#in) => _ = r#in.with_id(id),
Self::Delete(r#in) => _ = r#in.with_id(id),
Self::DeleteDo(r#in) => _ = r#in.with_id(id),
Self::Trash(r#in) => _ = r#in.with_id(id),
Self::TrashDo(r#in) => _ = r#in.with_id(id),
Self::Download(r#in) => _ = r#in.with_id(id),
Self::DownloadDo(r#in) => _ = r#in.with_id(id),
Self::Upload(r#in) => _ = r#in.with_id(id),
Self::UploadDo(r#in) => _ = r#in.with_id(id),
}
self
}
fn title(&self) -> Cow<'_, str> {
match self {
Self::Copy(r#in) => r#in.title(),
Self::CopyDo(r#in) => r#in.title(),
Self::Cut(r#in) => r#in.title(),
Self::CutDo(r#in) => r#in.title(),
Self::Link(r#in) => r#in.title(),
Self::LinkDo(r#in) => r#in.title(),
Self::Hardlink(r#in) => r#in.title(),
Self::HardlinkDo(r#in) => r#in.title(),
Self::Delete(r#in) => r#in.title(),
Self::DeleteDo(r#in) => r#in.title(),
Self::Trash(r#in) => r#in.title(),
Self::TrashDo(r#in) => r#in.title(),
Self::Download(r#in) => r#in.title(),
Self::DownloadDo(r#in) => r#in.title(),
Self::Upload(r#in) => r#in.title(),
Self::UploadDo(r#in) => r#in.title(),
}
}
}
impl_from_in! { impl_from_in! {
Copy(FileInCopy), Copy(FileInCopy),
Cut(FileInCut), Cut(FileInCut),
@ -36,27 +106,6 @@ impl_from_in! {
} }
impl FileIn { impl FileIn {
pub(crate) fn id(&self) -> Id {
match self {
Self::Copy(r#in) => r#in.id,
Self::CopyDo(r#in) => r#in.id,
Self::Cut(r#in) => r#in.id,
Self::CutDo(r#in) => r#in.id,
Self::Link(r#in) => r#in.id,
Self::LinkDo(r#in) => r#in.id,
Self::Hardlink(r#in) => r#in.id,
Self::HardlinkDo(r#in) => r#in.id,
Self::Delete(r#in) => r#in.id,
Self::DeleteDo(r#in) => r#in.id,
Self::Trash(r#in) => r#in.id,
Self::TrashDo(r#in) => r#in.id,
Self::Download(r#in) => r#in.id,
Self::DownloadDo(r#in) => r#in.id,
Self::Upload(r#in) => r#in.id,
Self::UploadDo(r#in) => r#in.id,
}
}
pub(crate) fn into_doable(self) -> Self { pub(crate) fn into_doable(self) -> Self {
match self { match self {
Self::Copy(r#in) => Self::CopyDo(r#in), Self::Copy(r#in) => Self::CopyDo(r#in),
@ -91,6 +140,21 @@ pub(crate) struct FileInCopy {
pub(crate) retry: u8, pub(crate) retry: u8,
} }
impl TaskIn for FileInCopy {
type Prog = FileProgCopy;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Copy {} to {}", self.from.display(), self.to.display()).into()
}
}
impl FileInCopy { impl FileInCopy {
pub(super) fn into_link(self) -> FileInLink { pub(super) fn into_link(self) -> FileInLink {
FileInLink { FileInLink {
@ -119,6 +183,21 @@ pub(crate) struct FileInCut {
pub(crate) drop: Option<mpsc::Sender<()>>, pub(crate) drop: Option<mpsc::Sender<()>>,
} }
impl TaskIn for FileInCut {
type Prog = FileProgCut;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Cut {} to {}", self.from.display(), self.to.display()).into()
}
}
impl Drop for FileInCut { impl Drop for FileInCut {
fn drop(&mut self) { _ = self.drop.take(); } fn drop(&mut self) { _ = self.drop.take(); }
} }
@ -156,6 +235,21 @@ pub(crate) struct FileInLink {
pub(crate) delete: bool, pub(crate) delete: bool,
} }
impl TaskIn for FileInLink {
type Prog = FileProgLink;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Link {} to {}", self.from.display(), self.to.display()).into()
}
}
// --- Hardlink // --- Hardlink
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct FileInHardlink { pub(crate) struct FileInHardlink {
@ -167,6 +261,21 @@ pub(crate) struct FileInHardlink {
pub(crate) follow: bool, pub(crate) follow: bool,
} }
impl TaskIn for FileInHardlink {
type Prog = FileProgHardlink;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Hardlink {} to {}", self.from.display(), self.to.display()).into()
}
}
// --- Delete // --- Delete
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct FileInDelete { pub(crate) struct FileInDelete {
@ -175,6 +284,19 @@ pub(crate) struct FileInDelete {
pub(crate) cha: Option<Cha>, pub(crate) cha: Option<Cha>,
} }
impl TaskIn for FileInDelete {
type Prog = FileProgDelete;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Delete {}", self.target.display()).into() }
}
// --- Trash // --- Trash
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct FileInTrash { pub(crate) struct FileInTrash {
@ -182,6 +304,19 @@ pub(crate) struct FileInTrash {
pub(crate) target: UrlBuf, pub(crate) target: UrlBuf,
} }
impl TaskIn for FileInTrash {
type Prog = FileProgTrash;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Trash {}", self.target.display()).into() }
}
// --- Download // --- Download
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct FileInDownload { pub(crate) struct FileInDownload {
@ -191,6 +326,19 @@ pub(crate) struct FileInDownload {
pub(crate) retry: u8, pub(crate) retry: u8,
} }
impl TaskIn for FileInDownload {
type Prog = FileProgDownload;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Download {}", self.target.display()).into() }
}
// --- Upload // --- Upload
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct FileInUpload { pub(crate) struct FileInUpload {
@ -199,3 +347,16 @@ pub(crate) struct FileInUpload {
pub(crate) cha: Option<Cha>, pub(crate) cha: Option<Cha>,
pub(crate) cache: Option<PathBuf>, pub(crate) cache: Option<PathBuf>,
} }
impl TaskIn for FileInUpload {
type Prog = FileProgUpload;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Upload {}", self.target.display()).into() }
}

View file

@ -1,6 +1,8 @@
use yazi_shared::{Id, url::UrlBuf}; use std::borrow::Cow;
use crate::{Task, TaskProg}; use yazi_shared::{Id, url::{UrlBuf, UrlLike}};
use crate::{Task, TaskIn, TaskProg};
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum HookIn { pub(crate) enum HookIn {
@ -27,32 +29,49 @@ impl_from_in!(
Preload(HookInPreload), Preload(HookInPreload),
); );
impl HookIn { impl TaskIn for HookIn {
pub(crate) fn id(&self) -> Id { type Prog = ();
fn id(&self) -> Id {
match self { match self {
Self::Copy(r#in) => r#in.id, Self::Copy(r#in) => r#in.id(),
Self::Cut(r#in) => r#in.id, Self::Cut(r#in) => r#in.id(),
Self::Delete(r#in) => r#in.id, Self::Delete(r#in) => r#in.id(),
Self::Trash(r#in) => r#in.id, Self::Trash(r#in) => r#in.id(),
Self::Link(r#in) => r#in.id, Self::Link(r#in) => r#in.id(),
Self::Hardlink(r#in) => r#in.id, Self::Hardlink(r#in) => r#in.id(),
Self::Download(r#in) => r#in.id, Self::Download(r#in) => r#in.id(),
Self::Upload(r#in) => r#in.id, Self::Upload(r#in) => r#in.id(),
Self::Preload(r#in) => r#in.id, Self::Preload(r#in) => r#in.id(),
} }
} }
pub(crate) fn with_id(self, id: Id) -> Self { fn with_id(&mut self, id: Id) -> &mut Self {
match self { match self {
Self::Copy(r#in) => Self::Copy(HookInOutCopy { id, ..r#in }), Self::Copy(r#in) => r#in.id = id,
Self::Cut(r#in) => Self::Cut(HookInOutCut { id, ..r#in }), Self::Cut(r#in) => r#in.id = id,
Self::Delete(r#in) => Self::Delete(HookInDelete { id, ..r#in }), Self::Delete(r#in) => r#in.id = id,
Self::Trash(r#in) => Self::Trash(HookInTrash { id, ..r#in }), Self::Trash(r#in) => r#in.id = id,
Self::Link(r#in) => Self::Link(HookInOutLink { id, ..r#in }), Self::Link(r#in) => r#in.id = id,
Self::Hardlink(r#in) => Self::Hardlink(HookInOutHardlink { id, ..r#in }), Self::Hardlink(r#in) => r#in.id = id,
Self::Download(r#in) => Self::Download(HookInDownload { id, ..r#in }), Self::Download(r#in) => r#in.id = id,
Self::Upload(r#in) => Self::Upload(HookInUpload { id, ..r#in }), Self::Upload(r#in) => r#in.id = id,
Self::Preload(r#in) => Self::Preload(HookInPreload { id, ..r#in }), Self::Preload(r#in) => r#in.id = id,
}
self
}
fn title(&self) -> Cow<'_, str> {
match self {
Self::Copy(r#in) => r#in.title(),
Self::Cut(r#in) => r#in.title(),
Self::Delete(r#in) => r#in.title(),
Self::Trash(r#in) => r#in.title(),
Self::Link(r#in) => r#in.title(),
Self::Hardlink(r#in) => r#in.title(),
Self::Download(r#in) => r#in.title(),
Self::Upload(r#in) => r#in.title(),
Self::Preload(r#in) => r#in.title(),
} }
} }
} }
@ -65,6 +84,21 @@ pub(crate) struct HookInOutCopy {
pub(crate) to: UrlBuf, pub(crate) to: UrlBuf,
} }
impl TaskIn for HookInOutCopy {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Hook: copy {} to {}", self.from.display(), self.to.display()).into()
}
}
impl HookInOutCopy { impl HookInOutCopy {
pub(crate) fn new<U>(from: U, to: U) -> Self pub(crate) fn new<U>(from: U, to: U) -> Self
where where
@ -88,6 +122,21 @@ pub(crate) struct HookInOutCut {
pub(crate) to: UrlBuf, pub(crate) to: UrlBuf,
} }
impl TaskIn for HookInOutCut {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Hook: cut {} to {}", self.from.display(), self.to.display()).into()
}
}
impl HookInOutCut { impl HookInOutCut {
pub(crate) fn new<U>(from: U, to: U) -> Self pub(crate) fn new<U>(from: U, to: U) -> Self
where where
@ -110,6 +159,19 @@ pub(crate) struct HookInDelete {
pub(crate) target: UrlBuf, pub(crate) target: UrlBuf,
} }
impl TaskIn for HookInDelete {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Hook: delete {}", self.target.display()).into() }
}
impl HookInDelete { impl HookInDelete {
pub(crate) fn new<U>(target: U) -> Self pub(crate) fn new<U>(target: U) -> Self
where where
@ -126,6 +188,19 @@ pub(crate) struct HookInTrash {
pub(crate) target: UrlBuf, pub(crate) target: UrlBuf,
} }
impl TaskIn for HookInTrash {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Hook: trash {}", self.target.display()).into() }
}
impl HookInTrash { impl HookInTrash {
pub(crate) fn new<U>(target: U) -> Self pub(crate) fn new<U>(target: U) -> Self
where where
@ -144,6 +219,21 @@ pub(crate) struct HookInOutLink {
pub(crate) to: UrlBuf, pub(crate) to: UrlBuf,
} }
impl TaskIn for HookInOutLink {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Hook: link {} to {}", self.from.display(), self.to.display()).into()
}
}
impl HookInOutLink { impl HookInOutLink {
pub(crate) fn new<U>(from: U, to: U) -> Self pub(crate) fn new<U>(from: U, to: U) -> Self
where where
@ -168,6 +258,21 @@ pub(crate) struct HookInOutHardlink {
pub(crate) to: UrlBuf, pub(crate) to: UrlBuf,
} }
impl TaskIn for HookInOutHardlink {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
format!("Hook: hardlink {} to {}", self.from.display(), self.to.display()).into()
}
}
impl HookInOutHardlink { impl HookInOutHardlink {
pub(crate) fn new<U>(from: U, to: U) -> Self pub(crate) fn new<U>(from: U, to: U) -> Self
where where
@ -190,6 +295,19 @@ pub(crate) struct HookInDownload {
pub(crate) target: UrlBuf, pub(crate) target: UrlBuf,
} }
impl TaskIn for HookInDownload {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Hook: download {}", self.target.display()).into() }
}
impl HookInDownload { impl HookInDownload {
pub(crate) fn new<U>(target: U) -> Self pub(crate) fn new<U>(target: U) -> Self
where where
@ -206,6 +324,19 @@ pub(crate) struct HookInUpload {
pub(crate) target: UrlBuf, pub(crate) target: UrlBuf,
} }
impl TaskIn for HookInUpload {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Hook: upload {}", self.target.display()).into() }
}
impl HookInUpload { impl HookInUpload {
pub(crate) fn new<U>(target: U) -> Self pub(crate) fn new<U>(target: U) -> Self
where where
@ -223,6 +354,19 @@ pub(crate) struct HookInPreload {
pub(crate) hash: u64, pub(crate) hash: u64,
} }
impl TaskIn for HookInPreload {
type Prog = ();
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Hook: run {}-th preloader", self.idx).into() }
}
impl HookInPreload { impl HookInPreload {
pub(crate) fn new(idx: u8, hash: u64) -> Self { Self { id: Id::ZERO, idx, hash } } pub(crate) fn new(idx: u8, hash: u64) -> Self { Self { id: Id::ZERO, idx, hash } }
} }

13
yazi-scheduler/src/in.rs Normal file
View file

@ -0,0 +1,13 @@
use std::borrow::Cow;
use yazi_shared::Id;
pub(crate) trait TaskIn {
type Prog;
fn id(&self) -> Id;
fn with_id(&mut self, id: Id) -> &mut Self;
fn title(&self) -> Cow<'_, str>;
}

View file

@ -2,7 +2,7 @@ mod macros;
yazi_macro::mod_pub!(fetch file hook plugin preload process size); yazi_macro::mod_pub!(fetch file hook plugin preload process size);
yazi_macro::mod_flat!(behavior cleanup ongoing op out progress proxy scheduler snap summary task worker); yazi_macro::mod_flat!(behavior cleanup ongoing op out progress proxy r#in scheduler snap summary task worker);
const LOW: u8 = yazi_config::Priority::Low as u8; const LOW: u8 = yazi_config::Priority::Low as u8;
const NORMAL: u8 = yazi_config::Priority::Normal as u8; const NORMAL: u8 = yazi_config::Priority::Normal as u8;

View file

@ -3,7 +3,7 @@ use yazi_config::YAZI;
use yazi_shared::{CompletionToken, Id, Ids}; use yazi_shared::{CompletionToken, Id, Ids};
use super::Task; use super::Task;
use crate::{TaskProg, hook::HookIn}; use crate::{TaskIn, TaskProg, hook::HookIn};
#[derive(Default)] #[derive(Default)]
pub struct Ongoing { pub struct Ongoing {
@ -11,11 +11,18 @@ pub struct Ongoing {
} }
impl Ongoing { impl Ongoing {
pub(super) fn add(&mut self, name: String, prog: TaskProg) -> &mut Task { pub(super) fn add<T>(&mut self, r#in: &mut T) -> &mut Task
where
T: TaskIn,
T::Prog: Into<TaskProg> + Default,
{
static IDS: Ids = Ids::new(); static IDS: Ids = Ids::new();
let id = IDS.next(); let id = IDS.next();
self.inner.entry(id).insert(Task::new(id, name, prog)).into_mut()
let title = r#in.with_id(id).title().into_owned();
let prog = T::Prog::default().into();
self.inner.entry(id).insert(Task::new(id, title, prog)).into_mut()
} }
pub(super) fn cancel(&mut self, id: Id) -> Option<HookIn> { pub(super) fn cancel(&mut self, id: Id) -> Option<HookIn> {

View file

@ -1,33 +1,90 @@
use std::ops::{Deref, DerefMut}; use std::borrow::Cow;
use hashbrown::HashMap;
use mlua::{ExternalError, FromLua, Lua, Value};
use yazi_dds::Sendable;
use yazi_runner::entry::EntryJob; use yazi_runner::entry::EntryJob;
use yazi_shared::Id; use yazi_shared::{Id, SStr, data::{Data, DataKey}};
use crate::{TaskIn, plugin::PluginProgEntry};
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum PluginIn { pub(crate) enum PluginIn {
Entry(PluginInEntry), Entry(PluginInEntry),
} }
impl_from_in!(Entry(PluginInEntry)); impl TaskIn for PluginIn {
type Prog = ();
impl PluginIn { fn id(&self) -> Id {
pub(crate) fn id(&self) -> Id {
match self { match self {
Self::Entry(r#in) => r#in.id, Self::Entry(r#in) => r#in.id(),
}
}
fn with_id(&mut self, id: Id) -> &mut Self {
match self {
Self::Entry(r#in) => _ = r#in.with_id(id),
}
self
}
fn title(&self) -> Cow<'_, str> {
match self {
Self::Entry(r#in) => r#in.title(),
} }
} }
} }
impl_from_in!(Entry(PluginInEntry));
// --- Entry // --- Entry
#[derive(Debug)] #[derive(Clone, Debug, Default)]
pub(crate) struct PluginInEntry(pub(crate) EntryJob); pub struct PluginInEntry {
pub id: Id,
impl Deref for PluginInEntry { pub plugin: SStr,
type Target = EntryJob; pub args: HashMap<DataKey, Data>,
pub title: SStr,
fn deref(&self) -> &Self::Target { &self.0 } pub track: bool,
} }
impl DerefMut for PluginInEntry { impl TaskIn for PluginInEntry {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } type Prog = PluginProgEntry;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> {
if self.title.is_empty() {
format!("Run plugin '{}'", self.plugin).into()
} else {
Cow::Borrowed(&self.title)
}
}
}
impl PluginInEntry {
pub(crate) fn into_job(self) -> EntryJob {
EntryJob { id: self.id, args: self.args, plugin: self.plugin }
}
}
impl FromLua for PluginInEntry {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let Value::Table(t) = value else {
return Err("constructing PluginInEntry from non-table value".into_lua_err());
};
Ok(Self {
id: Id::ZERO,
plugin: t.raw_get::<String>(1)?.into(),
args: Sendable::table_to_args(lua, t.raw_get("args")?)?,
title: t.raw_get::<Option<String>>("title")?.unwrap_or_default().into(),
track: t.raw_get("track")?,
})
}
} }

View file

@ -20,7 +20,7 @@ impl Plugin {
pub(crate) async fn entry(&self, task: PluginInEntry) -> Result<(), PluginOutEntry> { pub(crate) async fn entry(&self, task: PluginInEntry) -> Result<(), PluginOutEntry> {
let id = task.id; let id = task.id;
RUNNER.entry(task.0).await?; RUNNER.entry(task.into_job()).await?;
Ok(self.ops.out(id, PluginOutEntry::Succ)) Ok(self.ops.out(id, PluginOutEntry::Succ))
} }
} }

View file

@ -1,9 +1,26 @@
use std::borrow::Cow;
use yazi_config::plugin::Preloader; use yazi_config::plugin::Preloader;
use yazi_shared::Id; use yazi_shared::Id;
use crate::{TaskIn, preload::PreloadProg};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct PreloadIn { pub(crate) struct PreloadIn {
pub(crate) id: Id, pub(crate) id: Id,
pub(crate) plugin: &'static Preloader, pub(crate) plugin: &'static Preloader,
pub(crate) target: yazi_fs::File, pub(crate) target: yazi_fs::File,
} }
impl TaskIn for PreloadIn {
type Prog = PreloadProg;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Run preloader '{}'", self.plugin.run.name).into() }
}

View file

@ -47,7 +47,7 @@ impl Preload {
self.loaded.lock().get_mut(&hash).map(|x| *x &= !(1 << task.plugin.idx)); self.loaded.lock().get_mut(&hash).map(|x| *x &= !(1 << task.plugin.idx));
} }
if let Some(e) = state.error { if let Some(e) = state.error {
error!("Error when running preloader `{}`:\n{e}", task.plugin.run.name); error!("Error when running preloader '{}':\n{e}", task.plugin.run.name);
} }
Ok(self.ops.out(task.id, PreloadOut::Succ)) Ok(self.ops.out(task.id, PreloadOut::Succ))

View file

@ -1,8 +1,9 @@
use std::ffi::OsString; use std::{borrow::Cow, ffi::OsString};
use yazi_shared::{CompletionToken, Id, url::UrlCow}; use yazi_shared::{CompletionToken, Id, url::UrlCow};
use super::ShellOpt; use super::ShellOpt;
use crate::{TaskIn, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}};
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum ProcessIn { pub(crate) enum ProcessIn {
@ -13,14 +14,33 @@ pub(crate) enum ProcessIn {
impl_from_in!(Block(ProcessInBlock), Orphan(ProcessInOrphan), Bg(ProcessInBg)); impl_from_in!(Block(ProcessInBlock), Orphan(ProcessInOrphan), Bg(ProcessInBg));
impl ProcessIn { impl TaskIn for ProcessIn {
pub(crate) fn id(&self) -> Id { type Prog = ();
fn id(&self) -> Id {
match self { match self {
Self::Block(r#in) => r#in.id, Self::Block(r#in) => r#in.id,
Self::Orphan(r#in) => r#in.id, Self::Orphan(r#in) => r#in.id,
Self::Bg(r#in) => r#in.id, Self::Bg(r#in) => r#in.id,
} }
} }
fn with_id(&mut self, id: Id) -> &mut Self {
match self {
Self::Block(r#in) => _ = r#in.with_id(id),
Self::Orphan(r#in) => _ = r#in.with_id(id),
Self::Bg(r#in) => _ = r#in.with_id(id),
};
self
}
fn title(&self) -> Cow<'_, str> {
match self {
Self::Block(r#in) => r#in.title(),
Self::Orphan(r#in) => r#in.title(),
Self::Bg(r#in) => r#in.title(),
}
}
} }
// --- Block // --- Block
@ -32,6 +52,19 @@ pub(crate) struct ProcessInBlock {
pub(crate) args: Vec<UrlCow<'static>>, pub(crate) args: Vec<UrlCow<'static>>,
} }
impl TaskIn for ProcessInBlock {
type Prog = ProcessProgBlock;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Blocking command: {}", self.cmd.display()).into() }
}
impl From<ProcessInBlock> for ShellOpt { impl From<ProcessInBlock> for ShellOpt {
fn from(r#in: ProcessInBlock) -> Self { fn from(r#in: ProcessInBlock) -> Self {
Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: false, orphan: false } Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: false, orphan: false }
@ -47,6 +80,19 @@ pub(crate) struct ProcessInOrphan {
pub(crate) args: Vec<UrlCow<'static>>, pub(crate) args: Vec<UrlCow<'static>>,
} }
impl TaskIn for ProcessInOrphan {
type Prog = ProcessProgOrphan;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Orphan command: {}", self.cmd.display()).into() }
}
impl From<ProcessInOrphan> for ShellOpt { impl From<ProcessInOrphan> for ShellOpt {
fn from(r#in: ProcessInOrphan) -> Self { fn from(r#in: ProcessInOrphan) -> Self {
Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: false, orphan: true } Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: false, orphan: true }
@ -63,6 +109,19 @@ pub(crate) struct ProcessInBg {
pub(crate) done: CompletionToken, pub(crate) done: CompletionToken,
} }
impl TaskIn for ProcessInBg {
type Prog = ProcessProgBg;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Background command: {}", self.cmd.display()).into() }
}
impl From<ProcessInBg> for ShellOpt { impl From<ProcessInBg> for ShellOpt {
fn from(r#in: ProcessInBg) -> Self { fn from(r#in: ProcessInBg) -> Self {
Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: true, orphan: false } Self { cwd: r#in.cwd, cmd: r#in.cmd, args: r#in.args, piped: true, orphan: false }

View file

@ -1,13 +1,11 @@
use std::{ops::Deref, sync::Arc, time::Duration}; use std::{ops::Deref, sync::Arc, time::Duration};
use hashbrown::HashMap;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_config::{YAZI, plugin::{Fetcher, Preloader}};
use yazi_fs::FsHash64; use yazi_fs::FsHash64;
use yazi_runner::entry::EntryJob; use yazi_shared::{CompletionToken, Id, Throttle, url::{UrlBuf, UrlLike}};
use yazi_shared::{CompletionToken, Id, SStr, Throttle, data::{Data, DataKey}, url::{UrlBuf, UrlLike}};
use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskProg, Worker, fetch::{FetchIn, FetchProg}, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload, FileProgCopy, FileProgCut, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgTrash, FileProgUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::{PluginInEntry, PluginProgEntry}, preload::{PreloadIn, PreloadProg}, process::{ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOpt, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}, size::{SizeIn, SizeProg}}; use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskIn, TaskProg, Worker, fetch::FetchIn, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::PluginInEntry, preload::PreloadIn, process::{ProcessIn, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOpt}, size::SizeIn};
pub struct Scheduler { pub struct Scheduler {
pub worker: Worker, pub worker: Worker,
@ -27,14 +25,13 @@ impl Scheduler {
Self { worker, behavior: Behavior::new(), handles } Self { worker, behavior: Behavior::new(), handles }
} }
fn add<T, R>(&self, name: String, map: impl FnOnce(&mut Task) -> R) -> R fn add<T, R>(&self, r#in: &mut T, map: impl FnOnce(&mut Task) -> R) -> R
where where
T: Into<TaskProg> + Default, T: TaskIn,
T::Prog: Into<TaskProg> + Default,
{ {
let prog = T::default().into();
let mut ongoing = self.ongoing.lock(); let mut ongoing = self.ongoing.lock();
let task = ongoing.add(name, prog); let task = ongoing.add(r#in);
self.behavior.update(task.id); self.behavior.update(task.id);
map(task) map(task)
@ -42,14 +39,15 @@ impl Scheduler {
fn add_hooked<T, R>( fn add_hooked<T, R>(
&self, &self,
name: String, r#in: &mut T,
hook: impl Into<HookIn>, hook: impl Into<HookIn>,
map: impl FnOnce(&mut Task) -> R, map: impl FnOnce(&mut Task) -> R,
) -> R ) -> R
where where
T: Into<TaskProg> + Default, T: TaskIn,
T::Prog: Into<TaskProg> + Default,
{ {
self.add::<T, R>(name, |t| map(t.with_hook(hook))) self.add(r#in, |t| map(t.with_hook(hook)))
} }
pub fn cancel(&self, id: Id) -> bool { pub fn cancel(&self, id: Id) -> bool {
@ -68,111 +66,115 @@ impl Scheduler {
} }
pub fn file_cut(&self, from: UrlBuf, to: UrlBuf, force: bool) { pub fn file_cut(&self, from: UrlBuf, to: UrlBuf, force: bool) {
let name = format!("Cut {} to {}", from.display(), to.display());
let id = self.add::<FileProgCut, _>(name, |t| t.id);
if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
return self.ops.out(id, FileOutCut::Fail("Cannot cut directory into itself".to_owned()));
}
let follow = !from.scheme().covariant(to.scheme()); let follow = !from.scheme().covariant(to.scheme());
self let mut r#in =
.file FileInCut { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0, drop: None };
.submit(FileInCut { id, from, to, force, cha: None, follow, retry: 0, drop: None }, LOW);
self.add(&mut r#in, |_| ());
if r#in.to.try_starts_with(&r#in.from).unwrap_or(false) && !r#in.to.covariant(&r#in.from) {
self.ops.out(r#in.id, FileOutCut::Fail("Cannot cut directory into itself".to_owned()));
} else {
self.file.submit(r#in, LOW);
}
} }
pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) {
let name = format!("Copy {} to {}", from.display(), to.display());
let id = self.add::<FileProgCopy, _>(name, |t| t.id);
if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
return self.ops.out(id, FileOutCopy::Fail("Cannot copy directory into itself".to_owned()));
}
let follow = follow || !from.scheme().covariant(to.scheme()); let follow = follow || !from.scheme().covariant(to.scheme());
self.file.submit(FileInCopy { id, from, to, force, cha: None, follow, retry: 0 }, LOW); let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 };
self.add(&mut r#in, |_| ());
if r#in.to.try_starts_with(&r#in.from).unwrap_or(false) && !r#in.to.covariant(&r#in.from) {
self.ops.out(r#in.id, FileOutCopy::Fail("Cannot copy directory into itself".to_owned()));
} else {
self.file.submit(r#in, LOW);
}
} }
pub fn file_link(&self, from: UrlBuf, to: UrlBuf, relative: bool, force: bool) { pub fn file_link(&self, from: UrlBuf, to: UrlBuf, relative: bool, force: bool) {
let name = format!("Link {} to {}", from.display(), to.display()); let mut r#in = FileInLink {
let id = self.add::<FileProgLink, _>(name, |t| t.id); id: Id::ZERO,
from,
to,
force,
cha: None,
resolve: false,
relative,
delete: false,
};
self.file.submit( self.add(&mut r#in, |_| ());
FileInLink { id, from, to, force, cha: None, resolve: false, relative, delete: false }, self.file.submit(r#in, LOW);
LOW,
);
} }
pub fn file_hardlink(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { pub fn file_hardlink(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) {
let name = format!("Hardlink {} to {}", from.display(), to.display()); let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow };
let id = self.add::<FileProgHardlink, _>(name, |t| t.id); self.add(&mut r#in, |_| ());
if !from.scheme().covariant(to.scheme()) { if !r#in.from.scheme().covariant(r#in.to.scheme()) {
return self return self
.ops .ops
.out(id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned())); .out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned()));
} }
if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) { if r#in.to.try_starts_with(&r#in.from).unwrap_or(false) && !r#in.to.covariant(&r#in.from) {
return self return self
.ops .ops
.out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned())); .out(r#in.id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned()));
} }
self.file.submit(FileInHardlink { id, from, to, force, cha: None, follow }, LOW); self.file.submit(r#in, LOW);
} }
pub fn file_delete(&self, target: UrlBuf) { pub fn file_delete(&self, target: UrlBuf) {
let name = format!("Delete {}", target.display()); let mut r#in = FileInDelete { id: Id::ZERO, target, cha: None };
let hook = HookInDelete::new(&r#in.target);
let hook = HookInDelete::new(&target); self.add_hooked(&mut r#in, hook, |_| ());
let id = self.add_hooked::<FileProgDelete, _>(name, hook, |t| t.id); self.file.submit(r#in, LOW);
self.file.submit(FileInDelete { id, target, cha: None }, LOW);
} }
pub fn file_trash(&self, target: UrlBuf) { pub fn file_trash(&self, target: UrlBuf) {
let name = format!("Trash {}", target.display()); let mut r#in = FileInTrash { id: Id::ZERO, target };
let hook = HookInTrash::new(&r#in.target);
let hook = HookInTrash::new(&target); self.add_hooked(&mut r#in, hook, |_| ());
let id = self.add_hooked::<FileProgTrash, _>(name, hook, |t| t.id); self.file.submit(r#in, LOW);
self.file.submit(FileInTrash { id, target }, LOW);
} }
pub fn file_download(&self, target: UrlBuf) -> CompletionToken { pub fn file_download(&self, target: UrlBuf) -> CompletionToken {
let name = format!("Download {}", target.display()); let mut r#in = FileInDownload { id: Id::ZERO, target, cha: None, retry: 0 };
let hook = HookInDownload::new(&r#in.target);
let done = self.add_hooked(&mut r#in, hook, |t| t.done.clone());
let hook = HookInDownload::new(&target); if r#in.target.kind().is_remote() {
let (id, done) = self.add_hooked::<FileProgDownload, _>(name, hook, |t| (t.id, t.done.clone())); self.file.submit(r#in, LOW);
} else {
if !target.kind().is_remote() { self.ops.out(r#in.id, FileOutDownload::Fail("Cannot download non-remote file".to_owned()));
self.ops.out(id, FileOutDownload::Fail("Cannot download non-remote file".to_owned()));
return done;
} }
self.file.submit(FileInDownload { id, target, cha: None, retry: 0 }, LOW);
done done
} }
pub fn file_upload(&self, target: UrlBuf) { pub fn file_upload(&self, target: UrlBuf) {
let name = format!("Upload {}", target.display()); let mut r#in = FileInUpload { id: Id::ZERO, target, cha: None, cache: None };
let hook = HookInUpload::new(&r#in.target);
self.add_hooked(&mut r#in, hook, |_| ());
let hook = HookInUpload::new(&target); if r#in.target.kind().is_remote() {
let id = self.add_hooked::<FileProgUpload, _>(name, hook, |t| t.id); self.file.submit(r#in, LOW);
} else {
if !target.kind().is_remote() { self.ops.out(r#in.id, FileOutUpload::Fail("Cannot upload non-remote file".to_owned()));
return self.ops.out(id, FileOutUpload::Fail("Cannot upload non-remote file".to_owned()));
} }
self.file.submit(FileInUpload { id, target, cha: None, cache: None }, LOW);
} }
pub fn plugin_entry(&self, plugin: SStr, args: HashMap<DataKey, Data>) { pub fn plugin_entry(&self, mut r#in: PluginInEntry) -> Id {
let name = format!("Run micro plugin `{plugin}`"); if r#in.track {
let id = self.add::<PluginProgEntry, _>(name, |t| t.id); self.behavior.reset();
}
self.plugin.submit(PluginInEntry(EntryJob { id, args, plugin }), NORMAL); let id = self.add(&mut r#in, |t| t.id);
self.plugin.submit(r#in, NORMAL);
id
} }
pub fn fetch_paged( pub fn fetch_paged(
@ -180,10 +182,11 @@ impl Scheduler {
fetcher: &'static Fetcher, fetcher: &'static Fetcher,
targets: Vec<yazi_fs::File>, targets: Vec<yazi_fs::File>,
) -> CompletionToken { ) -> CompletionToken {
let name = format!("Run fetcher `{}` with {} target(s)", fetcher.run.name, targets.len()); let mut r#in = FetchIn { id: Id::ZERO, plugin: fetcher, targets };
let (id, done) = self.add::<FetchProg, _>(name, |t| (t.id, t.done.clone()));
let done = self.add(&mut r#in, |t| t.done.clone());
self.fetch.submit(r#in);
self.fetch.submit(FetchIn { id, plugin: fetcher, targets });
done done
} }
@ -202,61 +205,55 @@ impl Scheduler {
} }
pub fn preload_paged(&self, preloader: &'static Preloader, target: &yazi_fs::File) { pub fn preload_paged(&self, preloader: &'static Preloader, target: &yazi_fs::File) {
let name = format!("Run preloader `{}`", preloader.run.name); let mut r#in = PreloadIn { id: Id::ZERO, plugin: preloader, target: target.clone() };
let hook = HookInPreload::new(preloader.idx, target.hash_u64()); let hook = HookInPreload::new(preloader.idx, target.hash_u64());
let id = self.add_hooked::<PreloadProg, _>(name, hook, |t| t.id); self.add_hooked(&mut r#in, hook, |_| ());
if let Some(prev) = self.preload.loading.lock().put(target.url.hash_u64(), id) { if let Some(prev) = self.preload.loading.lock().put(target.url.hash_u64(), r#in.id) {
self.cancel(prev); self.cancel(prev);
} }
self.preload.submit(PreloadIn { id, plugin: preloader, target: target.clone() }); self.preload.submit(r#in);
} }
pub fn prework_size(&self, targets: Vec<&UrlBuf>) { pub fn prework_size(&self, targets: Vec<&UrlBuf>) {
let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300))); let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300)));
for target in targets { for target in targets {
let name = format!("Calculate the size of {}", target.display()); let mut r#in =
let id = self.add::<SizeProg, _>(name, |t| t.id); SizeIn { id: Id::ZERO, target: target.clone(), throttle: throttle.clone() };
self.size.submit(SizeIn { id, target: target.clone(), throttle: throttle.clone() }, NORMAL); self.add(&mut r#in, |_| ());
self.size.submit(r#in, NORMAL);
} }
} }
pub fn process_open(&self, opt: ProcessOpt) -> CompletionToken { pub fn process_open(&self, opt: ProcessOpt) -> CompletionToken {
let name = { let mut r#in: ProcessIn = if opt.block {
let args = opt.args.iter().map(|a| a.display().to_string()).collect::<Vec<_>>().join(" "); ProcessInBlock { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }.into()
if args.is_empty() { } else if opt.orphan {
format!("Run {:?}", opt.cmd) ProcessInOrphan { id: Id::ZERO, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }.into()
} else { } else {
format!("Run {:?} with `{args}`", opt.cmd) ProcessInBg {
id: Id::ZERO,
cwd: opt.cwd,
cmd: opt.cmd,
args: opt.args,
done: CompletionToken::default(),
}
.into()
};
let done = match &mut r#in {
ProcessIn::Block(r#in) => self.add(r#in, |t| t.done.clone()),
ProcessIn::Orphan(r#in) => self.add(r#in, |t| t.done.clone()),
ProcessIn::Bg(r#in) => {
r#in.done = self.add(r#in, |t| t.done.clone());
r#in.done.clone()
} }
}; };
let (id, done) = if opt.block { self.process.submit(r#in, NORMAL);
self.add::<ProcessProgBlock, _>(name, |t| (t.id, t.done.clone()))
} else if opt.orphan {
self.add::<ProcessProgOrphan, _>(name, |t| (t.id, t.done.clone()))
} else {
self.add::<ProcessProgBg, _>(name, |t| (t.id, t.done.clone()))
};
if opt.block {
self
.process
.submit(ProcessInBlock { id, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }, NORMAL);
} else if opt.orphan {
self
.process
.submit(ProcessInOrphan { id, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }, NORMAL);
} else {
self.process.submit(
ProcessInBg { id, cwd: opt.cwd, cmd: opt.cmd, args: opt.args, done: done.clone() },
NORMAL,
);
};
done done
} }
} }

View file

@ -1,6 +1,8 @@
use std::sync::Arc; use std::{borrow::Cow, sync::Arc};
use yazi_shared::{Id, Throttle, url::UrlBuf}; use yazi_shared::{Id, Throttle, url::{UrlBuf, UrlLike}};
use crate::{TaskIn, size::SizeProg};
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct SizeIn { pub(crate) struct SizeIn {
@ -9,6 +11,15 @@ pub(crate) struct SizeIn {
pub(crate) throttle: Arc<Throttle<(UrlBuf, u64)>>, pub(crate) throttle: Arc<Throttle<(UrlBuf, u64)>>,
} }
impl SizeIn { impl TaskIn for SizeIn {
pub(crate) fn id(&self) -> Id { self.id } type Prog = SizeProg;
fn id(&self) -> Id { self.id }
fn with_id(&mut self, id: Id) -> &mut Self {
self.id = id;
self
}
fn title(&self) -> Cow<'_, str> { format!("Size '{}'", self.target.display()).into() }
} }

View file

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

View file

@ -1,12 +1,12 @@
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_shared::{CompletionToken, Id}; use yazi_shared::{CompletionToken, Id};
use crate::{TaskProg, hook::HookIn}; use crate::{TaskIn, TaskProg, hook::HookIn};
#[derive(Debug)] #[derive(Debug)]
pub struct Task { pub struct Task {
pub id: Id, pub id: Id,
pub name: String, pub title: String,
pub(crate) prog: TaskProg, pub(crate) prog: TaskProg,
pub(crate) hook: Option<HookIn>, pub(crate) hook: Option<HookIn>,
pub done: CompletionToken, pub done: CompletionToken,
@ -16,10 +16,10 @@ pub struct Task {
} }
impl Task { impl Task {
pub(super) fn new(id: Id, name: String, prog: TaskProg) -> Self { pub(super) fn new(id: Id, title: String, prog: TaskProg) -> Self {
Self { Self {
id, id,
name, title,
prog, prog,
hook: None, hook: None,
done: Default::default(), done: Default::default(),
@ -39,7 +39,10 @@ impl Task {
} }
pub(super) fn with_hook(&mut self, hook: impl Into<HookIn>) -> &mut Self { pub(super) fn with_hook(&mut self, hook: impl Into<HookIn>) -> &mut Self {
self.hook = Some(hook.into().with_id(self.id)); let mut hook = hook.into();
hook.with_id(self.id);
self.hook = Some(hook);
self self
} }
} }

View file

@ -4,7 +4,7 @@ use parking_lot::Mutex;
use tokio::{select, sync::mpsc, task::JoinHandle}; use tokio::{select, sync::mpsc, task::JoinHandle};
use yazi_config::YAZI; use yazi_config::YAZI;
use crate::{CleanupState, LOW, Ongoing, Progress, TaskOp, TaskOps, TaskOut, fetch::{Fetch, FetchIn}, file::{File, FileIn}, hook::{Hook, HookIn}, plugin::{Plugin, PluginIn}, preload::{Preload, PreloadIn}, process::{Process, ProcessIn}, size::{Size, SizeIn}}; use crate::{CleanupState, LOW, Ongoing, Progress, TaskIn, TaskOp, TaskOps, TaskOut, fetch::{Fetch, FetchIn}, file::{File, FileIn}, hook::{Hook, HookIn}, plugin::{Plugin, PluginIn}, preload::{Preload, PreloadIn}, process::{Process, ProcessIn}, size::{Size, SizeIn}};
#[derive(Clone)] #[derive(Clone)]
pub struct Worker { pub struct Worker {

View file

@ -17,19 +17,32 @@ where
Ok(integer) Ok(integer)
} }
macro_rules! ref_or_owned {
(ref, $value:expr) => {
&$value
};
(owned, $value:expr) => {
$value
};
}
macro_rules! impl_into_integer { macro_rules! impl_into_integer {
($a:ty, $($b:ty),+ $(,)?) => { ($a:ty, $($b:ty),+ $(,)?) => {
impl_into_integer!(@impl ref $a, $a, $($b),+);
impl_into_integer!(@impl owned $a, &$a, $($b),+);
};
(@impl $kind:ident $t:ty, $a:ty, $($b:ty),+ $(,)?) => {
$( $(
impl TryFrom<&$a> for $b { impl TryFrom<$a> for $b {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: &$a) -> Result<Self, Self::Error> { fn try_from(value: $a) -> Result<Self, Self::Error> {
paste::paste! { paste::paste! {
Ok(match value { Ok(match ref_or_owned!($kind, value) {
$a::Integer(i) => <$b>::try_from(*i)?, $t::Integer(i) => <$b>::try_from(*i)?,
$a::Number(n) => <$b>::try_from($crate::data::macros::float_to_i64(*n)?)?, $t::Number(n) => <$b>::try_from($crate::data::macros::float_to_i64(*n)?)?,
$a::String(s) => s.parse()?, $t::String(s) => s.parse()?,
$a::Id(i) => <$b>::try_from(i.get())?, $t::Id(i) => <$b>::try_from(i.get())?,
_ => anyhow::bail!("not an integer"), _ => anyhow::bail!("not an integer"),
}) })
} }
@ -41,17 +54,21 @@ macro_rules! impl_into_integer {
macro_rules! impl_into_number { macro_rules! impl_into_number {
($a:ty, $($b:ty),+ $(,)?) => { ($a:ty, $($b:ty),+ $(,)?) => {
impl_into_number!(@impl ref $a, $a, $($b),+);
impl_into_number!(@impl owned $a, &$a, $($b),+);
};
(@impl $kind:ident $t:ty, $a:ty, $($b:ty),+ $(,)?) => {
$( $(
impl TryFrom<&$a> for $b { impl TryFrom<$a> for $b {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: &$a) -> Result<Self, Self::Error> { fn try_from(value: $a) -> Result<Self, Self::Error> {
paste::paste! { paste::paste! {
Ok(match value { Ok(match ref_or_owned!($kind, value) {
$a::Integer(i) if *i == (*i as $b as _) => *i as $b, $t::Integer(i) if *i == (*i as $b as _) => *i as $b,
$a::Number(n) if f64::from(*n) == (f64::from(*n) as $b as _) => f64::from(*n) as $b, $t::Number(n) if f64::from(*n) == (f64::from(*n) as $b as _) => f64::from(*n) as $b,
$a::String(s) => s.parse()?, $t::String(s) => s.parse()?,
$a::Id(i) if i.get() == (i.get() as $b as _) => i.get() as $b, $t::Id(i) if i.get() == (i.get() as $b as _) => i.get() as $b,
_ => anyhow::bail!("not a number"), _ => anyhow::bail!("not a number"),
}) })
} }

View file

@ -27,6 +27,7 @@ futures = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }

View file

@ -1,6 +1,6 @@
use std::{num::ParseIntError, str::FromStr}; use std::{fmt, num::ParseIntError, str::FromStr};
use yazi_shared::data::Data; use serde::{Deserialize, Deserializer, de};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Step { pub enum Step {
@ -35,15 +35,57 @@ impl FromStr for Step {
} }
} }
impl TryFrom<&Data> for Step { impl<'de> Deserialize<'de> for Step {
type Error = ParseIntError; fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
fn try_from(value: &Data) -> Result<Self, Self::Error> { impl de::Visitor<'_> for Visitor {
Ok(match value { type Value = Step;
Data::Integer(i) => Self::from(*i as isize),
Data::String(s) => s.parse()?, fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
_ => "".parse()?, formatter.write_str("a step string or integer offset")
}) }
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse().map_err(E::custom)
}
fn visit_borrowed_str<E>(self, value: &'_ str) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(value)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(&value)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
isize::try_from(value).map(Self::Value::from).map_err(E::custom)
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
isize::try_from(value).map(Self::Value::from).map_err(E::custom)
}
}
deserializer.deserialize_any(Visitor)
} }
} }