feat: allow preloaders to return an optional Error to describe the failure (#2253)

This commit is contained in:
三咲雅 · Misaki Masa 2025-01-28 03:09:45 +08:00 committed by sxyazi
parent da36cd6ab8
commit c061397a09
No known key found for this signature in database
42 changed files with 297 additions and 230 deletions

2
Cargo.lock generated
View file

@ -3510,6 +3510,7 @@ dependencies = [
"bitflags 2.8.0",
"core-foundation-sys",
"dirs",
"foldhash",
"futures",
"libc",
"objc",
@ -3592,6 +3593,7 @@ dependencies = [
"async-priority-channel",
"futures",
"libc",
"lru",
"parking_lot",
"scopeguard",
"tokio",

View file

@ -18,10 +18,12 @@ clap = { version = "4.5.27", features = [ "derive" ] }
core-foundation-sys = "0.8.7"
crossterm = { version = "0.28.1", features = [ "event-stream" ] }
dirs = "6.0.0"
foldhash = "0.1.4"
futures = "0.3.31"
globset = "0.4.15"
indexmap = { version = "2.7.1", features = [ "serde" ] }
libc = "0.2.169"
lru = "0.12.5"
md-5 = "0.10.6"
mlua = { version = "0.10.2", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
objc = "0.2.7"

View file

@ -91,7 +91,7 @@ suppress_preload = false
fetchers = [
# Mimetype
{ id = "mime", name = "*", run = "mime", if = "!mime", prio = "high" },
{ id = "mime", name = "*", run = "mime", prio = "high" },
]
spotters = [
{ name = "*/", run = "folder" },

View file

@ -1,7 +1,7 @@
use std::path::Path;
use serde::Deserialize;
use yazi_shared::{Condition, MIME_DIR, event::Cmd};
use yazi_shared::{MIME_DIR, event::Cmd};
use crate::{Pattern, Priority};
@ -11,8 +11,6 @@ pub struct Fetcher {
pub idx: u8,
pub id: String,
#[serde(rename = "if")]
pub if_: Option<Condition>,
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Cmd,
@ -22,9 +20,8 @@ pub struct Fetcher {
impl Fetcher {
#[inline]
pub fn matches(&self, path: &Path, mime: &str, f: impl Fn(&str) -> bool + Copy) -> bool {
self.if_.as_ref().and_then(|c| c.eval(f)) != Some(false)
&& (self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)))
pub fn matches(&self, path: &Path, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR))
}
}

View file

@ -20,11 +20,10 @@ impl Plugin {
&'b self,
path: &'a Path,
mime: &'a str,
factor: impl Fn(&str) -> bool + Copy + 'a,
) -> impl Iterator<Item = &'b Fetcher> + 'a {
let mut seen = HashSet::new();
self.fetchers.iter().filter(move |&f| {
if seen.contains(&f.id) || !f.matches(path, mime, factor) {
if seen.contains(&f.id) || !f.matches(path, mime) {
return false;
}
seen.insert(&f.id);
@ -35,12 +34,7 @@ impl Plugin {
pub fn mime_fetchers(&self, files: Vec<File>) -> impl Iterator<Item = (&Fetcher, Vec<File>)> {
let mut tasks: [Vec<_>; MAX_PREWORKERS as usize] = Default::default();
for f in files {
let factors = |s: &str| match s {
"dummy" => f.cha.is_dummy(),
_ => false,
};
let found = self.fetchers.iter().find(|&g| g.id == "mime" && g.matches(&f.url, "", factors));
let found = self.fetchers.iter().find(|&g| g.id == "mime" && g.matches(&f.url, ""));
if let Some(g) = found {
tasks[g.idx as usize].push(f);
} else {
@ -53,11 +47,6 @@ impl Plugin {
})
}
#[inline]
pub fn fetchers_mask(&self) -> u32 {
self.fetchers.iter().fold(0, |n, f| if f.mime.is_some() { n } else { n | 1 << f.idx as u32 })
}
pub fn spotter(&self, path: &Path, mime: &str) -> Option<&Spotter> {
self.spotters.iter().find(|&p| p.matches(path, mime))
}

View file

@ -57,7 +57,9 @@ impl Manager {
if repeek {
self.peek(false);
}
tasks.prework_affected(&affected, &self.mimetype);
tasks.fetch_paged(&affected, &self.mimetype);
tasks.preload_paged(&affected, &self.mimetype);
render!();
}
}

View file

@ -6,12 +6,10 @@ use parking_lot::RwLock;
use tokio::{fs, pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tracing::error;
use yazi_config::PLUGIN;
use yazi_dds::Pubsub;
use yazi_fs::{Cha, File, Files, FilesOp, mounts::PARTITIONS, realname_unchecked};
use yazi_plugin::isolate;
use yazi_proxy::WATCHER;
use yazi_shared::{RoCell, event::CmdCow, url::Url};
use yazi_shared::{RoCell, url::Url};
use super::Linked;
use crate::tab::Folder;
@ -129,7 +127,6 @@ impl Watcher {
let _permit = WATCHER.acquire().await.unwrap();
let mut ops = Vec::with_capacity(urls.len());
let mut reload = Vec::with_capacity(urls.len());
for u in urls {
let Some((parent, urn)) = u.pair() else { continue };
@ -147,18 +144,10 @@ impl Watcher {
continue;
}
if !file.is_dir() {
reload.push(file.clone());
}
ops.push(FilesOp::Upserting(parent, HashMap::from_iter([(urn, file)])));
}
FilesOp::mutate(ops);
for (fetcher, files) in PLUGIN.mime_fetchers(reload) {
if let Err(e) = isolate::fetch(CmdCow::from(&fetcher.run), files).await {
error!("Fetch mime failed in watcher: {e}");
}
}
}
}

View file

@ -9,18 +9,12 @@ impl Tasks {
let mut loaded = self.scheduler.prework.loaded.lock();
let mut tasks: [Vec<_>; MAX_PREWORKERS as usize] = Default::default();
for f in paged {
let mime = mimetype.by_file(f).unwrap_or_default();
let factors = |s: &str| match s {
"mime" => !mime.is_empty(),
"dummy" => f.cha.is_dummy(),
_ => false,
};
for g in PLUGIN.fetchers(&f.url, mime, factors) {
match loaded.get_mut(&f.url) {
let hash = f.hash();
for g in PLUGIN.fetchers(&f.url, mimetype.by_file(f).unwrap_or_default()) {
match loaded.get_mut(&hash) {
Some(n) if *n & (1 << g.idx) != 0 => continue,
Some(n) => *n |= 1 << g.idx,
None => _ = loaded.insert(f.url_owned(), 1 << g.idx),
None => _ = loaded.put(hash, 1 << g.idx),
}
tasks[g.idx as usize].push(f.clone());
}
@ -37,31 +31,18 @@ impl Tasks {
pub fn preload_paged(&self, paged: &[File], mimetype: &Mimetype) {
let mut loaded = self.scheduler.prework.loaded.lock();
for f in paged {
let mime = mimetype.by_file(f).unwrap_or_default();
for p in PLUGIN.preloaders(&f.url, mime) {
match loaded.get_mut(&f.url) {
let hash = f.hash();
for p in PLUGIN.preloaders(&f.url, mimetype.by_file(f).unwrap_or_default()) {
match loaded.get_mut(&hash) {
Some(n) if *n & (1 << p.idx) != 0 => continue,
Some(n) => *n |= 1 << p.idx,
None => _ = loaded.insert(f.url_owned(), 1 << p.idx),
None => _ = loaded.put(hash, 1 << p.idx),
}
self.scheduler.preload_paged(p, f);
}
}
}
pub fn prework_affected(&self, affected: &[File], mimetype: &Mimetype) {
let mask = PLUGIN.fetchers_mask();
{
let mut loaded = self.scheduler.prework.loaded.lock();
for f in affected {
loaded.get_mut(&f.url).map(|n| *n &= mask);
}
}
self.fetch_paged(affected, mimetype);
self.preload_paged(affected, mimetype);
}
pub fn prework_sorted(&self, targets: &Files) {
if targets.sorter().by != SortBy::Size {
return;

View file

@ -2,7 +2,7 @@ use std::ops::{Deref, Range};
use mlua::{AnyUserData, Lua, UserData, UserDataFields};
use yazi_config::LAYOUT;
use yazi_plugin::{bindings::Cast, url::Url};
use yazi_plugin::url::Url;
use super::{File, Files, Lives};
@ -50,7 +50,7 @@ impl Folder {
impl UserData for Folder {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cwd", |lua, me| Url::cast(lua, me.url.to_owned()));
fields.add_field_method_get("cwd", |_, me| Ok(Url(me.url.to_owned())));
fields.add_field_method_get("files", |_, me| Files::make(0..me.files.len(), me, me.tab()));
fields.add_field_method_get("stage", |lua, me| lua.create_any_userdata(me.stage));
fields.add_field_method_get("window", |_, me| Files::make(me.window.clone(), me, me.tab()));

View file

@ -2,7 +2,7 @@ use std::ops::Deref;
use indexmap::{IndexMap, map::Keys};
use mlua::{AnyUserData, IntoLuaMulti, MetaMethod, UserData, UserDataMethods, UserDataRefMut};
use yazi_plugin::{bindings::Cast, url::Url};
use yazi_plugin::url::Url;
use super::{Iter, Lives};
@ -35,7 +35,7 @@ impl UserData for Selected {
let iter = lua.create_function(
|lua, mut iter: UserDataRefMut<Iter<Keys<yazi_shared::url::Url, u64>, _>>| {
if let Some(next) = iter.next() {
(next.0, Url::cast(lua, next.1.clone())?).into_lua_multi(lua)
(next.0, Url(next.1.clone())).into_lua_multi(lua)
} else {
().into_lua_multi(lua)
}

View file

@ -1,7 +1,7 @@
use std::{collections::hash_set, ops::Deref};
use mlua::{AnyUserData, IntoLuaMulti, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRefMut};
use yazi_plugin::{bindings::Cast, url::Url};
use yazi_plugin::url::Url;
use super::{Iter, Lives};
@ -37,7 +37,7 @@ impl UserData for Yanked {
let iter = lua.create_function(
|lua, mut iter: UserDataRefMut<Iter<hash_set::Iter<yazi_shared::url::Url>, _>>| {
if let Some(next) = iter.next() {
(next.0, Url::cast(lua, next.1.clone())?).into_lua_multi(lua)
(next.0, Url(next.1.clone())).into_lua_multi(lua)
} else {
().into_lua_multi(lua)
}

View file

@ -18,6 +18,7 @@ anyhow = { workspace = true }
arc-swap = "1.7.1"
bitflags = { workspace = true }
dirs = { workspace = true }
foldhash = { workspace = true }
futures = { workspace = true }
parking_lot = { workspace = true }
regex = { workspace = true }

View file

@ -1,4 +1,4 @@
use std::{ffi::OsStr, fs::{FileType, Metadata}, ops::Deref};
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref};
use anyhow::Result;
use tokio::fs;
@ -48,6 +48,19 @@ impl File {
}
}
#[inline]
pub fn hash(&self) -> u64 {
let mut h = foldhash::fast::FixedState::default().build_hasher();
self.url.hash(&mut h);
h.write_u8(0);
self.cha.len.hash(&mut h);
h.write_u8(0);
self.cha.mtime.hash(&mut h);
h.write_u8(0);
self.cha.btime.hash(&mut h);
h.finish()
}
#[inline]
pub fn rebase(&self, parent: &Url) -> Self {
Self {

View file

@ -4,7 +4,12 @@ local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache or self:preload(job) ~= 1 then
if not cache then
return
end
local ok, err = self:preload(job)
if not ok or err then
return
end
@ -18,7 +23,7 @@ function M:seek() end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return 1
return true
end
local status, err = Command("magick"):args({
@ -40,10 +45,9 @@ function M:preload(job)
}):status()
if status then
return status.success and 1 or 2
return status.success
else
ya.err("Failed to start `magick`, error: " .. err)
return 0
return true, Err("Failed to start `magick`, error: %s", err)
end
end

View file

@ -16,10 +16,10 @@ function M:seek() end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return 1
return true
end
return ya.image_precache(job.file.url, cache) and 1 or 2
return ya.image_precache(job.file.url, cache)
end
function M:spot(job)

View file

@ -2,7 +2,12 @@ local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache or self:preload(job) ~= 1 then
if not cache then
return
end
local ok, err = self:preload(job)
if not ok or err then
return
end
@ -16,7 +21,7 @@ function M:seek() end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return 1
return true
end
local status, err = Command("magick")
@ -36,10 +41,9 @@ function M:preload(job)
:status()
if status then
return status.success and 1 or 2
return status.success
else
ya.err("Failed to start `magick`, error: " .. err)
return 0
return true, Err("Failed to start `magick`, error: %s", err)
end
end

View file

@ -18,8 +18,7 @@ function M:fetch(job)
local cmd = os.getenv("YAZI_FILE_ONE") or "file"
local child, err = Command(cmd):args({ "-bL", "--mime-type", "--" }):args(urls):stdout(Command.PIPED):spawn()
if not child then
ya.err(string.format("Failed to start `%s`, error: %s", cmd, err))
return 0
return true, Err("Failed to start `%s`, error: %s", cmd, err)
end
local updates, last = {}, ya.time()
@ -33,7 +32,7 @@ function M:fetch(job)
end
end
local i, j, valid = 1, 0, nil
local i, valid, state = 1, nil, {}
repeat
local line, event = child:read_line_with { timeout = 300 }
if event == 3 then
@ -45,8 +44,10 @@ function M:fetch(job)
valid = match_mimetype(line)
if valid then
j, updates[urls[i]] = j + 1, valid
updates[urls[i]], state[i] = valid, true
flush(false)
else
state[i] = false
end
i = i + 1
@ -54,7 +55,7 @@ function M:fetch(job)
until i > #urls
flush(true)
return j == #urls and 3 or 2
return state
end
return M

View file

@ -4,9 +4,9 @@ function M:peek() end
function M:seek() end
function M:fetch() return 1 end
function M:fetch() return true end
function M:preload() return 1 end
function M:preload() return true end
function M:spot() end

View file

@ -2,7 +2,12 @@ local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache or self:preload(job) ~= 1 then
if not cache then
return
end
local ok, err = self:preload(job)
if not ok or err then
return
end
@ -22,10 +27,10 @@ end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return 1
return true
end
local output = Command("pdftoppm")
local output, err = Command("pdftoppm")
:args({
"-singlefile",
"-jpeg",
@ -40,16 +45,16 @@ function M:preload(job)
:output()
if not output then
return 0
return true, Err("Failed to start `pdftoppm`, error: %s", err)
elseif not output.status.success then
local pages = tonumber(output.stderr:match("the last page %((%d+)%)")) or 0
if job.skip > 0 and pages > 0 then
ya.manager_emit("peek", { math.max(0, pages - 1), only_if = job.file.url, upper_bound = true })
end
return 0
return true, Err("Failed to convert PDF to image, stderr: %s", output.stderr)
end
return fs.write(cache, output.stdout) and 1 or 2
return fs.write(cache, output.stdout)
end
return M

View file

@ -2,7 +2,12 @@ local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache or self:preload(job) ~= 1 then
if not cache then
return
end
local ok, err = self:preload(job)
if not ok or err then
return
end
@ -25,25 +30,24 @@ function M:preload(job)
local percent = 5 + job.skip
if percent > 95 then
ya.manager_emit("peek", { 90, only_if = job.file.url, upper_bound = true })
return 2
return false
end
local cache = ya.file_cache(job)
if not cache then
return 1
return true
end
local cha = fs.cha(cache)
if cha and cha.len > 0 then
return 1
return true
end
local meta, err = self.list_meta(job.file.url, "format=duration")
if not meta then
ya.err(tostring(err))
return 0
return true, err
elseif not meta.format.duration then
return 0
return true, Err("Failed to get video duration")
end
local ss = math.floor(meta.format.duration * percent / 100)
@ -62,10 +66,9 @@ function M:preload(job)
}):status()
if status then
return status.success and 1 or 2
return status.success
else
ya.err("Failed to start `ffmpeg`, error: " .. err)
return 0
return true, Err("Failed to start `ffmpeg`, error: %s", err)
end
end
@ -115,14 +118,14 @@ function M.list_meta(url, entries)
local output, err =
Command("ffprobe"):args({ "-v", "quiet", "-show_entries", entries, "-of", "json=c=1", tostring(url) }):output()
if not output then
return nil, Err("Failed to start `ffprobe`, error: " .. err)
return nil, Err("Failed to start `ffprobe`, error: %s", err)
end
local t = ya.json_decode(output.stdout)
if not t then
return nil, Err("Failed to decode `ffprobe` output: " .. output.stdout)
return nil, Err("Failed to decode `ffprobe` output: %s", output.stdout)
elseif type(t) ~= "table" then
return nil, Err("Invalid `ffprobe` output: " .. output.stdout)
return nil, Err("Invalid `ffprobe` output: %s", output.stdout)
end
t.format = t.format or {}

View file

@ -1,5 +0,0 @@
use mlua::Lua;
pub trait Cast<T> {
fn cast(lua: &Lua, data: T) -> mlua::Result<mlua::AnyUserData>;
}

View file

@ -1,3 +1,3 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(bindings cha chan icon image input layer mouse permit range window);
yazi_macro::mod_flat!(cha chan icon image input layer mouse permit range window);

View file

@ -8,12 +8,7 @@ pub(super) struct Plugin;
impl Plugin {
pub(super) fn fetchers(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, (file, mime): (FileRef, mlua::String)| {
let factors = |s: &str| match s {
"mime" => !mime.as_bytes().is_empty(),
"dummy" => file.cha.is_dummy(),
_ => false,
};
lua.create_sequence_from(PLUGIN.fetchers(&file.url, &mime.to_str()?, factors).map(Fetcher))
lua.create_sequence_from(PLUGIN.fetchers(&file.url, &mime.to_str()?).map(Fetcher))
})
}

View file

@ -1,7 +1,7 @@
use std::mem;
use ansi_to_tui::IntoText;
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use unicode_width::UnicodeWidthChar;
use super::Span;
@ -12,7 +12,6 @@ const RIGHT: u8 = 2;
const EXPECTED: &str = "expected a string, Span, Line, or a table of them";
#[derive(Clone, FromLua)]
pub struct Line(pub(super) ratatui::text::Line<'static>);
impl Line {

View file

@ -1,8 +1,10 @@
use mlua::{AnyUserData, FromLua, Lua, MetaMethod, Table, UserData};
use mlua::{AnyUserData, ExternalError, Lua, MetaMethod, Table, UserData, Value};
use super::Cell;
#[derive(Clone, Debug, Default, FromLua)]
const EXPECTED: &str = "expected a Row";
#[derive(Clone, Debug, Default)]
pub struct Row {
pub(super) cells: Vec<Cell>,
height: u16,
@ -34,6 +36,23 @@ impl From<Row> for ratatui::widgets::Row<'static> {
}
}
impl TryFrom<Value> for Row {
type Error = mlua::Error;
fn try_from(value: Value) -> Result<Self, Self::Error> {
Ok(match value {
Value::UserData(ud) => {
if let Ok(row) = ud.take() {
row
} else {
Err(EXPECTED.into_lua_err())?
}
}
_ => Err(EXPECTED.into_lua_err())?,
})
}
}
impl UserData for Row {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
crate::impl_style_method!(methods, style);

View file

@ -1,9 +1,8 @@
use mlua::{ExternalError, FromLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{ExternalError, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use unicode_width::UnicodeWidthChar;
const EXPECTED: &str = "expected a string or Span";
#[derive(Clone, FromLua)]
pub struct Span(pub(super) ratatui::text::Span<'static>);
impl Span {

View file

@ -1,9 +1,11 @@
use mlua::{AnyUserData, Lua, MetaMethod, UserData, Value};
use mlua::{AnyUserData, ExternalError, Lua, MetaMethod, UserData, Value};
use ratatui::widgets::StatefulWidget;
use super::{Area, Row};
use crate::elements::{Constraint, Style};
const EXPECTED: &str = "expected a table of Rows";
// --- Table
#[derive(Clone, Debug, Default)]
pub struct Table {
@ -31,7 +33,12 @@ pub struct Table {
impl Table {
pub fn compose(lua: &Lua) -> mlua::Result<mlua::Table> {
let new = lua.create_function(|_, (_, rows): (mlua::Table, Vec<Row>)| {
let new = lua.create_function(|_, (_, seq): (mlua::Table, mlua::Table)| {
let mut rows = Vec::with_capacity(seq.raw_len());
for v in seq.sequence_values::<Value>() {
rows.push(Row::try_from(v?).map_err(|_| EXPECTED.into_lua_err())?);
}
Ok(Self { rows, ..Default::default() })
})?;
@ -96,12 +103,12 @@ impl UserData for Table {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
crate::impl_area_method!(methods);
methods.add_function_mut("header", |_, (ud, row): (AnyUserData, Row)| {
ud.borrow_mut::<Self>()?.header = Some(row.into());
methods.add_function_mut("header", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.header = Some(Row::try_from(value)?.into());
Ok(ud)
});
methods.add_function_mut("footer", |_, (ud, row): (AnyUserData, Row)| {
ud.borrow_mut::<Self>()?.footer = Some(row.into());
methods.add_function_mut("footer", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.footer = Some(Row::try_from(value)?.into());
Ok(ud)
});
methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec<Constraint>)| {

View file

@ -1,7 +1,7 @@
use std::mem;
use ansi_to_tui::IntoText;
use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value};
use mlua::{ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, Table, UserData, Value};
use ratatui::widgets::Widget;
use super::{Area, Line, Span};
@ -18,7 +18,7 @@ pub const WRAP_TRIM: u8 = 2;
const EXPECTED: &str = "expected a string, Line, Span, or a table of them";
#[derive(Clone, Debug, Default, FromLua)]
#[derive(Clone, Debug, Default)]
pub struct Text {
pub area: Area,

View file

@ -1,6 +1,8 @@
use std::borrow::Cow;
use std::{borrow::Cow, fmt::Display};
use mlua::{ExternalError, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
use mlua::{ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
const EXPECTED: &str = "expected a Error";
pub enum Error {
Io(std::io::Error),
@ -24,6 +26,25 @@ impl Error {
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(e) => write!(f, "{e}"),
Error::Serde(e) => write!(f, "{e}"),
Error::Custom(s) => write!(f, "{s}"),
}
}
}
impl FromLua for Error {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => ud.take(),
_ => Err(EXPECTED.into_lua_err()),
}
}
}
impl UserData for Error {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("code", |_, me| {

View file

@ -1,10 +1,10 @@
use mlua::{AnyUserData, Lua, Table, UserDataRef};
use mlua::{AnyUserData, IntoLua, Lua, Table, UserDataRef};
use crate::{bindings::{Cast, Cha}, impl_file_fields, impl_file_methods};
use crate::{bindings::Cha, impl_file_fields, impl_file_methods};
pub type FileRef = UserDataRef<yazi_fs::File>;
pub struct File;
pub struct File(pub yazi_fs::File);
impl File {
#[inline]
@ -18,17 +18,19 @@ impl File {
pub fn install(lua: &Lua) -> mlua::Result<()> {
lua.globals().raw_set(
"File",
lua.create_function(|lua, t: Table| {
Self::cast(lua, yazi_fs::File {
lua.create_function(|_, t: Table| {
Ok(Self(yazi_fs::File {
url: t.raw_get::<AnyUserData>("url")?.take()?,
cha: *t.raw_get::<Cha>("cha")?,
..Default::default()
})
}))
})?,
)
}
}
impl<T: Into<yazi_fs::File>> Cast<T> for File {
fn cast(lua: &Lua, data: T) -> mlua::Result<AnyUserData> { lua.create_any_userdata(data.into()) }
impl IntoLua for File {
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
lua.create_any_userdata(self.0)?.into_lua(lua)
}
}

View file

@ -3,7 +3,7 @@ use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua,
use tokio::fs;
use yazi_fs::{mounts::PARTITIONS, remove_dir_clean};
use crate::{Composer, Error, bindings::{Cast, Cha}, file::File, url::{Url, UrlRef}};
use crate::{Composer, Error, bindings::Cha, file::File, url::{Url, UrlRef}};
pub fn compose(lua: &Lua) -> mlua::Result<Value> {
Composer::make(lua, 10, |lua, key| {
@ -24,7 +24,7 @@ pub fn compose(lua: &Lua) -> mlua::Result<Value> {
fn cwd(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, ()| match std::env::current_dir() {
Ok(p) => (Url::cast(lua, p)?, Value::Nil).into_lua_multi(lua),
Ok(p) => (Url::from(p), Value::Nil).into_lua_multi(lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(lua),
})
}
@ -129,7 +129,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
} else {
yazi_fs::File::from_dummy(url, next.file_type().await.ok())
};
files.push(File::cast(&lua, file)?);
files.push(File(file));
}
let tbl = lua.create_table_with_capacity(files.len(), 0)?;
@ -144,7 +144,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
fn unique_name(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
match yazi_fs::unique_name(url.clone(), async { false }).await {
Ok(u) => (Url::cast(&lua, u)?, Value::Nil).into_lua_multi(&lua),
Ok(u) => (Url(u), Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
})

View file

@ -1,15 +1,18 @@
use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table};
use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, ObjectLike, Table, Value};
use tokio::runtime::Handle;
use yazi_config::LAYOUT;
use yazi_dds::Sendable;
use yazi_shared::event::CmdCow;
use super::slim_lua;
use crate::{bindings::Cast, elements::Rect, file::File, loader::LOADER};
use crate::{Error, elements::Rect, file::File, loader::LOADER};
pub async fn fetch(cmd: CmdCow, files: Vec<yazi_fs::File>) -> mlua::Result<u8> {
pub async fn fetch(
cmd: CmdCow,
files: Vec<yazi_fs::File>,
) -> mlua::Result<(FetchState, Option<Error>)> {
if files.is_empty() {
return Ok(1);
return Ok((FetchState::Bool(true), None));
}
LOADER.ensure(&cmd.name).await.into_lua_err()?;
@ -21,22 +24,45 @@ pub async fn fetch(cmd: CmdCow, files: Vec<yazi_fs::File>) -> mlua::Result<u8> {
return Err("unloaded plugin".into_lua_err());
};
let files =
lua.create_sequence_from(files.into_iter().filter_map(|f| File::cast(&lua, f).ok()))?;
if files.raw_len() == 0 {
return Ok(1);
}
Handle::current().block_on(plugin.call_async_method(
"fetch",
lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("files", files.into_lua(&lua)?),
("files", lua.create_sequence_from(files.into_iter().map(File))?.into_lua(&lua)?),
])?,
))
})
.await
.into_lua_err()?
}
// --- State
pub enum FetchState {
Bool(bool),
Vec(Vec<bool>),
}
impl FetchState {
#[inline]
pub fn get(&self, idx: usize) -> bool {
match self {
Self::Bool(b) => *b,
Self::Vec(v) => v.get(idx).copied().unwrap_or(false),
}
}
}
impl FromLua for FetchState {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
Ok(match value {
Value::Boolean(b) => Self::Bool(b),
Value::Table(tbl) => Self::Vec(tbl.sequence_values().collect::<mlua::Result<_>>()?),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "FetchState".to_owned(),
message: Some("expected a boolean or a table of booleans".to_owned()),
})?,
})
}
}

View file

@ -10,7 +10,7 @@ use yazi_proxy::{AppProxy, options::{PluginCallback, PluginOpt}};
use yazi_shared::event::Cmd;
use super::slim_lua;
use crate::{bindings::Cast, elements::Rect, file::File, loader::LOADER};
use crate::{elements::Rect, file::File, loader::LOADER};
pub fn peek(
cmd: &'static Cmd,
@ -46,7 +46,7 @@ pub fn peek(
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("file", File::cast(&lua, file)?.into_lua(&lua)?),
("file", File(file).into_lua(&lua)?),
("mime", mime.into_lua(&lua)?),
("skip", skip.into_lua(&lua)?),
])?;
@ -76,7 +76,7 @@ pub fn peek_sync(cmd: &'static Cmd, file: yazi_fs::File, mime: Cow<'static, str>
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(lua)?),
("args", Sendable::args_to_table_ref(lua, &cmd.args)?.into_lua(lua)?),
("file", File::cast(lua, file)?.into_lua(lua)?),
("file", File(file).into_lua(lua)?),
("mime", mime.into_lua(lua)?),
("skip", skip.into_lua(lua)?),
])?;

View file

@ -1,13 +1,16 @@
use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table};
use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table, Value};
use tokio::runtime::Handle;
use yazi_config::LAYOUT;
use yazi_dds::Sendable;
use yazi_shared::event::Cmd;
use super::slim_lua;
use crate::{bindings::Cast, elements::Rect, file::File, loader::LOADER};
use crate::{Error, deprecate, elements::Rect, file::File, loader::LOADER};
pub async fn preload(cmd: &'static Cmd, file: yazi_fs::File) -> mlua::Result<u8> {
pub async fn preload(
cmd: &'static Cmd,
file: yazi_fs::File,
) -> mlua::Result<(bool, Option<Error>)> {
LOADER.ensure(&cmd.name).await.into_lua_err()?;
tokio::task::spawn_blocking(move || {
@ -21,11 +24,29 @@ pub async fn preload(cmd: &'static Cmd, file: yazi_fs::File) -> mlua::Result<u8>
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("file", File::cast(&lua, file)?.into_lua(&lua)?),
("file", File(file).into_lua(&lua)?),
("skip", 0.into_lua(&lua)?),
])?;
Handle::current().block_on(plugin.call_async_method("preload", job))
let (ok, mut err): (Value, Option<Error>) =
Handle::current().block_on(plugin.call_async_method("preload", job))?;
// TODO: remove this
let ok = match ok {
Value::Boolean(b) => b,
Value::Integer(n) => {
deprecate!(lua, "The integer return value of `preload()` has been deprecated since 25.01.27, please use the new `(bool, error)` instead, in your {}.
See #2253 for more information: https://github.com/sxyazi/yazi/pull/2253");
if n as u8 & 1 == 0 {
err = Some(Error::Custom(format!("Returned {n} when running the preloader")));
}
n as u8 & 1 == 1
},
_ => Err("The first return value of `preload()` must be a bool".into_lua_err())?,
};
Ok((ok, err))
})
.await
.into_lua_err()?

View file

@ -3,12 +3,12 @@ use yazi_config::LAYOUT;
use yazi_proxy::{AppProxy, options::{PluginCallback, PluginOpt}};
use yazi_shared::event::Cmd;
use crate::{bindings::Cast, elements::Rect, file::File};
use crate::{elements::Rect, file::File};
pub fn seek_sync(cmd: &'static Cmd, file: yazi_fs::File, units: i16) {
let cb: PluginCallback = Box::new(move |lua, plugin| {
let job = lua.create_table_from([
("file", File::cast(lua, file)?.into_lua(lua)?),
("file", File(file).into_lua(lua)?),
("area", Rect::from(LAYOUT.get().preview).into_lua(lua)?),
("units", units.into_lua(lua)?),
])?;

View file

@ -8,7 +8,7 @@ use yazi_dds::Sendable;
use yazi_shared::event::Cmd;
use super::slim_lua;
use crate::{bindings::Cast, file::File, loader::LOADER};
use crate::{file::File, loader::LOADER};
pub fn spot(
cmd: &'static Cmd,
@ -43,7 +43,7 @@ pub fn spot(
let job = lua.create_table_from([
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("file", File::cast(&lua, file)?.into_lua(&lua)?),
("file", File(file).into_lua(&lua)?),
("mime", mime.into_lua(&lua)?),
("skip", skip.into_lua(&lua)?),
])?;

View file

@ -82,13 +82,11 @@ macro_rules! impl_style_shorthands {
macro_rules! impl_file_fields {
($fields:ident) => {
use mlua::UserDataFields;
use $crate::bindings::Cast;
$fields.add_field_method_get("cha", |_, me| Ok($crate::bindings::Cha::from(me.cha)));
$fields.add_field_method_get("url", |lua, me| $crate::url::Url::cast(lua, me.url_owned()));
$fields.add_field_method_get("link_to", |lua, me| {
me.link_to.clone().map(|u| $crate::url::Url::cast(lua, u)).transpose()
});
$fields.add_field_method_get("url", |_, me| Ok($crate::url::Url(me.url_owned())));
$fields
.add_field_method_get("link_to", |_, me| Ok(me.link_to.clone().map(|u| $crate::url::Url(u))));
$fields.add_field_method_get("name", |lua, me| {
Some(me.name())

View file

@ -1,10 +1,8 @@
use mlua::{AnyUserData, ExternalError, Lua, MetaMethod, UserDataFields, UserDataMethods, UserDataRef, Value};
use crate::bindings::Cast;
use mlua::{ExternalError, IntoLua, Lua, MetaMethod, UserDataFields, UserDataMethods, UserDataRef, Value};
pub type UrlRef = UserDataRef<yazi_shared::url::Url>;
pub struct Url;
pub struct Url(pub yazi_shared::url::Url);
impl Url {
pub fn register(lua: &Lua) -> mlua::Result<()> {
@ -25,16 +23,14 @@ impl Url {
reg.add_method("ext", |lua, me, ()| {
me.extension().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
});
reg.add_method("join", |lua, me, other: Value| {
Self::cast(lua, match other {
reg.add_method("join", |_, me, other: Value| {
Ok(Self(match other {
Value::String(s) => me.join(s.to_str()?.as_ref()),
Value::UserData(ud) => me.join(&*ud.borrow::<yazi_shared::url::Url>()?),
_ => Err("must be a string or a Url".into_lua_err())?,
})
});
reg.add_method("parent", |lua, me, ()| {
me.parent_url().map(|u| Self::cast(lua, u)).transpose()
}))
});
reg.add_method("parent", |_, me, ()| Ok(me.parent_url().map(Self)));
reg.add_method("starts_with", |_, me, base: Value| {
Ok(match base {
Value::String(s) => me.starts_with(s.to_str()?.as_ref()),
@ -49,13 +45,13 @@ impl Url {
_ => Err("must be a string or a Url".into_lua_err())?,
})
});
reg.add_method("strip_prefix", |lua, me, base: Value| {
reg.add_method("strip_prefix", |_, me, base: Value| {
let path = match base {
Value::String(s) => me.strip_prefix(s.to_str()?.as_ref()),
Value::UserData(ud) => me.strip_prefix(&*ud.borrow::<yazi_shared::url::Url>()?),
_ => Err("must be a string or a Url".into_lua_err())?,
};
path.ok().map(|p| Self::cast(lua, yazi_shared::url::Url::from(p))).transpose()
Ok(path.ok().map(Self::from))
});
reg.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me == &*other));
@ -71,13 +67,17 @@ impl Url {
pub fn install(lua: &Lua) -> mlua::Result<()> {
lua.globals().raw_set(
"Url",
lua.create_function(|lua, url: mlua::String| {
Self::cast(lua, yazi_shared::url::Url::from(url.to_str()?.as_ref()))
})?,
lua.create_function(|_, url: mlua::String| Ok(Self::from(url.to_str()?.as_ref())))?,
)
}
}
impl<T: Into<yazi_shared::url::Url>> Cast<T> for Url {
fn cast(lua: &Lua, data: T) -> mlua::Result<AnyUserData> { lua.create_any_userdata(data.into()) }
impl<T: Into<yazi_shared::url::Url>> From<T> for Url {
fn from(value: T) -> Self { Self(value.into()) }
}
impl IntoLua for Url {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
lua.create_any_userdata(self.0)?.into_lua(lua)
}
}

View file

@ -3,11 +3,11 @@ use twox_hash::XxHash3_128;
use yazi_config::PREVIEW;
use super::Utils;
use crate::{bindings::Cast, file::FileRef, url::Url};
use crate::{file::FileRef, url::Url};
impl Utils {
pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, t: Table| {
lua.create_function(|_, t: Table| {
let file: FileRef = t.raw_get("file")?;
if file.url.parent() == Some(&PREVIEW.cache_dir) {
return Ok(None);
@ -20,7 +20,7 @@ impl Utils {
format!("{:x}", h.finish_128())
};
Some(Url::cast(lua, PREVIEW.cache_dir.join(hex))).transpose()
Ok(Some(Url::from(PREVIEW.cache_dir.join(hex))))
})
}
}

View file

@ -8,6 +8,7 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result<Value> {
Composer::make(lua, 40, move |lua, key| {
match key {
// App
b"__250127" => Utils::hide(lua)?, // TODO: remove this
b"hide" => Utils::hide(lua)?,
// Cache

View file

@ -21,6 +21,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.4.3" }
anyhow = { workspace = true }
async-priority-channel = "0.2.0"
futures = { workspace = true }
lru = { workspace = true }
parking_lot = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }

View file

@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::{collections::{HashMap, HashSet}, num::NonZeroUsize};
use anyhow::{Result, anyhow};
use lru::LruCache;
use parking_lot::{Mutex, RwLock};
use tokio::sync::mpsc;
use tracing::error;
@ -16,7 +17,7 @@ pub struct Prework {
macro_: async_priority_channel::Sender<TaskOp, u8>,
prog: mpsc::UnboundedSender<TaskProg>,
pub loaded: Mutex<HashMap<Url, u32>>,
pub loaded: Mutex<LruCache<u64, u32>>,
pub size_loading: RwLock<HashSet<Url>>,
}
@ -25,60 +26,49 @@ impl Prework {
macro_: async_priority_channel::Sender<TaskOp, u8>,
prog: mpsc::UnboundedSender<TaskProg>,
) -> Self {
Self { macro_, prog, loaded: Default::default(), size_loading: Default::default() }
Self {
macro_,
prog,
loaded: Mutex::new(LruCache::new(NonZeroUsize::new(4096).unwrap())),
size_loading: Default::default(),
}
}
pub async fn work(&self, op: PreworkOp) -> Result<()> {
match op {
PreworkOp::Fetch(task) => {
let urls: Vec<_> = task.targets.iter().map(|f| f.url_owned()).collect();
let hashes: Vec<_> = task.targets.iter().map(|f| f.hash()).collect();
let result = isolate::fetch(CmdCow::from(&task.plugin.run), task.targets).await;
if let Err(e) = result {
self.fail(
task.id,
format!(
"Failed to run fetcher `{}` with:\n{}\n\nError message:\n{e}",
task.plugin.run.name,
urls.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n")
),
)?;
self.fail(task.id, format!("Failed to run fetcher `{}`:\n{e}", task.plugin.run.name))?;
return Err(e.into());
};
let code = result.unwrap();
if code & 1 == 0 {
error!(
"Returned {code} when running fetcher `{}` with:\n{}",
task.plugin.run.name,
urls.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n")
);
let (state, err) = result.unwrap();
let mut loaded = self.loaded.lock();
for (_, h) in hashes.into_iter().enumerate().filter(|&(i, _)| !state.get(i)) {
loaded.get_mut(&h).map(|x| *x &= !(1 << task.plugin.idx));
}
if code & 2 != 0 {
let mut loaded = self.loaded.lock();
for url in urls {
loaded.get_mut(&url).map(|x| *x &= !(1 << task.plugin.idx));
}
if let Some(e) = err {
error!("Error when running fetcher `{}`:\n{e}", task.plugin.run.name);
}
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
}
PreworkOp::Load(task) => {
let url = task.target.url_owned();
let hash = task.target.hash();
let result = isolate::preload(&task.plugin.run, task.target).await;
if let Err(e) = result {
self.fail(
task.id,
format!("Failed to run preloader `{}` with `{url}`:\n{e}", task.plugin.run.name),
)?;
self
.fail(task.id, format!("Failed to run preloader `{}`:\n{e}", task.plugin.run.name))?;
return Err(e.into());
};
let code = result.unwrap();
if code & 1 == 0 {
error!("Returned {code} when running preloader `{}` with `{url}`", task.plugin.run.name);
let (ok, err) = result.unwrap();
if !ok {
self.loaded.lock().get_mut(&hash).map(|x| *x &= !(1 << task.plugin.idx));
}
if code & 2 != 0 {
let mut loaded = self.loaded.lock();
loaded.get_mut(&url).map(|x| *x &= !(1 << task.plugin.idx));
if let Some(e) = err {
error!("Error when running preloader `{}`:\n{e}", task.plugin.run.name);
}
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
}