feat: new null plugin as a general-purpose error handler (#3387)

This commit is contained in:
三咲雅 misaki masa 2025-11-30 14:44:17 +08:00 committed by GitHub
parent 76196aab70
commit 0fb652d2ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 211 additions and 130 deletions

View file

@ -14,9 +14,20 @@ pub enum Error {
impl Error {
pub fn install(lua: &Lua) -> mlua::Result<()> {
let new = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?;
let custom = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?;
lua.globals().raw_set("Error", lua.create_table_from([("custom", new)])?)
let fs = lua.create_function(|_, value: Value| {
Ok(Self::Fs(match value {
Value::Table(t) => yazi_fs::error::Error::custom(
&t.raw_get::<mlua::String>("kind")?.to_str()?,
t.raw_get("code")?,
&t.raw_get::<mlua::String>("message")?.to_str()?,
)?,
_ => Err("expected a table".into_lua_err())?,
}))
})?;
lua.globals().raw_set("Error", lua.create_table_from([("custom", custom), ("fs", fs)])?)
}
pub fn custom(msg: impl Into<SStr>) -> Self { Self::Custom(msg.into()) }
@ -60,6 +71,13 @@ impl UserData for Error {
_ => None,
})
});
fields.add_field_method_get("kind", |_, me| {
Ok(match me {
Self::Io(e) => Some(yazi_fs::error::Error::from(e.kind()).kind_str()),
Self::Fs(e) => Some(e.kind_str()),
_ => None,
})
});
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -114,6 +114,8 @@ spotters = [
{ mime = "video/*", run = "video" },
# Virtual file system
{ mime = "vfs/*", run = "vfs" },
# Error
{ mime = "null/*", run = "null" },
# Fallback
{ url = "*", run = "file" },
]
@ -160,6 +162,8 @@ previewers = [
{ mime = "inode/empty", run = "empty" },
# Virtual file system
{ mime = "vfs/*", run = "vfs" },
# Error
{ mime = "null/*", run = "null" },
# Fallback
{ url = "*", run = "file" },
]

View file

@ -1,5 +1,9 @@
use std::{fmt, io, sync::Arc};
use anyhow::Result;
use crate::error::{kind_from_str, kind_to_str};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Error {
Kind(io::ErrorKind),
@ -38,6 +42,10 @@ impl fmt::Display for Error {
}
impl Error {
pub fn custom(kind: &str, code: Option<i32>, message: &str) -> Result<Self> {
Ok(Self::Custom { kind: kind_from_str(kind)?, code, message: message.into() })
}
pub fn kind(&self) -> io::ErrorKind {
match self {
Self::Kind(kind) => *kind,
@ -46,6 +54,8 @@ impl Error {
}
}
pub fn kind_str(&self) -> &'static str { kind_to_str(self.kind()) }
pub fn raw_os_error(&self) -> Option<i32> {
match self {
Self::Kind(_) => None,

View file

@ -1,103 +1,104 @@
use std::io;
use anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::Error;
fn kind_to_str(kind: io::ErrorKind) -> &'static str {
pub(super) fn kind_to_str(kind: io::ErrorKind) -> &'static str {
use std::io::ErrorKind as K;
match kind {
K::NotFound => "not_found",
K::PermissionDenied => "permission_denied",
K::ConnectionRefused => "connection_refused",
K::ConnectionReset => "connection_reset",
K::HostUnreachable => "host_unreachable",
K::NetworkUnreachable => "network_unreachable",
K::ConnectionAborted => "connection_aborted",
K::NotConnected => "not_connected",
K::AddrInUse => "addr_in_use",
K::AddrNotAvailable => "addr_not_available",
K::NetworkDown => "network_down",
K::BrokenPipe => "broken_pipe",
K::AlreadyExists => "already_exists",
K::WouldBlock => "would_block",
K::NotADirectory => "not_a_directory",
K::IsADirectory => "is_a_directory",
K::DirectoryNotEmpty => "directory_not_empty",
K::ReadOnlyFilesystem => "read_only_filesystem",
// K::FilesystemLoop => "filesystem_loop",
K::StaleNetworkFileHandle => "stale_network_file_handle",
K::InvalidInput => "invalid_input",
K::InvalidData => "invalid_data",
K::TimedOut => "timed_out",
K::WriteZero => "write_zero",
K::StorageFull => "storage_full",
K::NotSeekable => "not_seekable",
K::QuotaExceeded => "quota_exceeded",
K::FileTooLarge => "file_too_large",
K::ResourceBusy => "resource_busy",
K::ExecutableFileBusy => "executable_file_busy",
K::Deadlock => "deadlock",
K::CrossesDevices => "crosses_devices",
K::TooManyLinks => "too_many_links",
K::InvalidFilename => "invalid_filename",
K::ArgumentListTooLong => "argument_list_too_long",
K::Interrupted => "interrupted",
K::Unsupported => "unsupported",
K::UnexpectedEof => "unexpected_eof",
K::OutOfMemory => "out_of_memory",
// K::InProgress => "in_progress",
K::Other => "other",
_ => "other",
K::NotFound => "NotFound",
K::PermissionDenied => "PermissionDenied",
K::ConnectionRefused => "ConnectionRefused",
K::ConnectionReset => "ConnectionReset",
K::HostUnreachable => "HostUnreachable",
K::NetworkUnreachable => "NetworkUnreachable",
K::ConnectionAborted => "ConnectionAborted",
K::NotConnected => "NotConnected",
K::AddrInUse => "AddrInUse",
K::AddrNotAvailable => "AddrNotAvailable",
K::NetworkDown => "NetworkDown",
K::BrokenPipe => "BrokenPipe",
K::AlreadyExists => "AlreadyExists",
K::WouldBlock => "WouldBlock",
K::NotADirectory => "NotADirectory",
K::IsADirectory => "IsADirectory",
K::DirectoryNotEmpty => "DirectoryNotEmpty",
K::ReadOnlyFilesystem => "ReadOnlyFilesystem",
// K::FilesystemLoop => "FilesystemLoop",
K::StaleNetworkFileHandle => "StaleNetworkFileHandle",
K::InvalidInput => "InvalidInput",
K::InvalidData => "InvalidData",
K::TimedOut => "TimedOut",
K::WriteZero => "WriteZero",
K::StorageFull => "StorageFull",
K::NotSeekable => "NotSeekable",
K::QuotaExceeded => "QuotaExceeded",
K::FileTooLarge => "FileTooLarge",
K::ResourceBusy => "ResourceBusy",
K::ExecutableFileBusy => "ExecutableFileBusy",
K::Deadlock => "Deadlock",
K::CrossesDevices => "CrossesDevices",
K::TooManyLinks => "TooManyLinks",
K::InvalidFilename => "InvalidFilename",
K::ArgumentListTooLong => "ArgumentListTooLong",
K::Interrupted => "Interrupted",
K::Unsupported => "Unsupported",
K::UnexpectedEof => "UnexpectedEof",
K::OutOfMemory => "OutOfMemory",
// K::InProgress => "InProgress",
K::Other => "Other",
_ => "Other",
}
}
fn kind_from_str(s: &str) -> io::ErrorKind {
pub(super) fn kind_from_str(s: &str) -> Result<io::ErrorKind> {
use std::io::ErrorKind as K;
match s {
"not_found" => K::NotFound,
"permission_denied" => K::PermissionDenied,
"connection_refused" => K::ConnectionRefused,
"connection_reset" => K::ConnectionReset,
"host_unreachable" => K::HostUnreachable,
"network_unreachable" => K::NetworkUnreachable,
"connection_aborted" => K::ConnectionAborted,
"not_connected" => K::NotConnected,
"addr_in_use" => K::AddrInUse,
"addr_not_available" => K::AddrNotAvailable,
"network_down" => K::NetworkDown,
"broken_pipe" => K::BrokenPipe,
"already_exists" => K::AlreadyExists,
"would_block" => K::WouldBlock,
"not_a_directory" => K::NotADirectory,
"is_a_directory" => K::IsADirectory,
"directory_not_empty" => K::DirectoryNotEmpty,
"read_only_filesystem" => K::ReadOnlyFilesystem,
// "filesystem_loop" => K::FilesystemLoop,
"stale_network_file_handle" => K::StaleNetworkFileHandle,
"invalid_input" => K::InvalidInput,
"invalid_data" => K::InvalidData,
"timed_out" => K::TimedOut,
"write_zero" => K::WriteZero,
"storage_full" => K::StorageFull,
"not_seekable" => K::NotSeekable,
"quota_exceeded" => K::QuotaExceeded,
"file_too_large" => K::FileTooLarge,
"resource_busy" => K::ResourceBusy,
"executable_file_busy" => K::ExecutableFileBusy,
"deadlock" => K::Deadlock,
"crosses_devices" => K::CrossesDevices,
"too_many_links" => K::TooManyLinks,
"invalid_filename" => K::InvalidFilename,
"argument_list_too_long" => K::ArgumentListTooLong,
"interrupted" => K::Interrupted,
"unsupported" => K::Unsupported,
"unexpected_eof" => K::UnexpectedEof,
"out_of_memory" => K::OutOfMemory,
// "in_progress" => K::InProgress,
"other" => K::Other,
_ => K::Other,
}
Ok(match s {
"NotFound" => K::NotFound,
"PermissionDenied" => K::PermissionDenied,
"ConnectionRefused" => K::ConnectionRefused,
"ConnectionReset" => K::ConnectionReset,
"HostUnreachable" => K::HostUnreachable,
"NetworkUnreachable" => K::NetworkUnreachable,
"ConnectionAborted" => K::ConnectionAborted,
"NotConnected" => K::NotConnected,
"AddrInUse" => K::AddrInUse,
"AddrNotAvailable" => K::AddrNotAvailable,
"NetworkDown" => K::NetworkDown,
"BrokenPipe" => K::BrokenPipe,
"AlreadyExists" => K::AlreadyExists,
"WouldBlock" => K::WouldBlock,
"NotADirectory" => K::NotADirectory,
"IsADirectory" => K::IsADirectory,
"DirectoryNotEmpty" => K::DirectoryNotEmpty,
"ReadOnlyFilesystem" => K::ReadOnlyFilesystem,
// "FilesystemLoop" => K::FilesystemLoop,
"StaleNetworkFileHandle" => K::StaleNetworkFileHandle,
"InvalidInput" => K::InvalidInput,
"InvalidData" => K::InvalidData,
"TimedOut" => K::TimedOut,
"WriteZero" => K::WriteZero,
"StorageFull" => K::StorageFull,
"NotSeekable" => K::NotSeekable,
"QuotaExceeded" => K::QuotaExceeded,
"FileTooLarge" => K::FileTooLarge,
"ResourceBusy" => K::ResourceBusy,
"ExecutableFileBusy" => K::ExecutableFileBusy,
"Deadlock" => K::Deadlock,
"CrossesDevices" => K::CrossesDevices,
"TooManyLinks" => K::TooManyLinks,
"InvalidFilename" => K::InvalidFilename,
"ArgumentListTooLong" => K::ArgumentListTooLong,
"Interrupted" => K::Interrupted,
"Unsupported" => K::Unsupported,
"UnexpectedEof" => K::UnexpectedEof,
"OutOfMemory" => K::OutOfMemory,
// "InProgress" => K::InProgress,
"Other" => K::Other,
_ => bail!("unknown error kind: {s}"),
})
}
impl Serialize for Error {
@ -138,15 +139,19 @@ impl<'de> Deserialize<'de> for Error {
let shadow = Shadow::deserialize(deserializer)?;
Ok(match shadow {
Shadow::Kind { kind } => Self::Kind(kind_from_str(&kind)),
Shadow::Kind { kind } => Self::Kind(kind_from_str(&kind).map_err(serde::de::Error::custom)?),
Shadow::Raw { code } => Self::Raw(code),
Shadow::Dyn { kind, code, message } => {
if !message.is_empty() {
Self::Custom { kind: kind_from_str(&kind), code, message: message.into() }
Self::Custom {
kind: kind_from_str(&kind).map_err(serde::de::Error::custom)?,
code,
message: message.into(),
}
} else if let Some(code) = code {
Self::Raw(code)
} else {
Self::Kind(kind_from_str(&kind))
Self::Kind(kind_from_str(&kind).map_err(serde::de::Error::custom)?)
}
}
})

View file

@ -3,41 +3,6 @@ local TYPE_PATS = { "text", "image", "video", "application", "audio", "font", "i
local M = {}
local function match_mimetype(line)
for _, pat in ipairs(TYPE_PATS) do
local typ, sub = line:match(string.format("(%s/)([+-.a-zA-Z0-9]+)%%s+$", pat))
if not sub then
elseif line:find(typ .. sub, 1, true) == 1 then
return typ:gsub("^x%-", "", 1) .. sub:gsub("^x%-", "", 1):gsub("^vnd%.", "", 1)
else
return nil, true
end
end
end
local function spawn_file1(paths)
local bin = os.getenv("YAZI_FILE_ONE") or "file"
local windows = ya.target_family() == "windows"
local cmd = Command(bin):arg({ "-bL", "--mime-type" }):stdout(Command.PIPED)
if windows then
cmd:arg({ "-f", "-" }):stdin(Command.PIPED)
else
cmd:arg("--"):arg(paths)
end
local child, err = cmd:spawn()
if not child then
return nil, Err("Failed to start `%s`, error: %s", bin, err)
elseif windows then
child:write_all(table.concat(paths, "\n"))
child:flush()
ya.drop(child:take_stdin())
end
return child
end
function M:fetch(job)
local urls, paths = {}, {}
for i, file in ipairs(job.files) do
@ -48,8 +13,9 @@ function M:fetch(job)
end
end
local child, err = spawn_file1(paths)
local child, err = M.spawn_file1(paths)
if not child then
M.placeholder(err, urls, paths)
return true, err
end
@ -74,7 +40,7 @@ function M:fetch(job)
break
end
match, ignore = match_mimetype(line)
match, ignore = M.match_mimetype(line)
if match then
updates[urls[i] or paths[i]], state[i], i = match, true, i + 1
flush(false)
@ -88,4 +54,59 @@ function M:fetch(job)
return state
end
function M.match_mimetype(line)
for _, pat in ipairs(TYPE_PATS) do
local typ, sub = line:match(string.format("(%s/)([+-.a-zA-Z0-9]+)%%s+$", pat))
if not sub then
elseif line:find(typ .. sub, 1, true) == 1 then
return typ:gsub("^x%-", "", 1) .. sub:gsub("^x%-", "", 1):gsub("^vnd%.", "", 1)
else
return nil, true
end
end
end
function M.file1_bin() return os.getenv("YAZI_FILE_ONE") or "file" end
function M.spawn_file1(paths)
local bin = M.file1_bin()
local windows = ya.target_family() == "windows"
local cmd = Command(bin):arg({ "-bL", "--mime-type" }):stdout(Command.PIPED)
if windows then
cmd:arg({ "-f", "-" }):stdin(Command.PIPED)
else
cmd:arg("--"):arg(paths)
end
local child, err = cmd:spawn()
if not child then
local e = Error.fs {
kind = err.kind or "Other",
code = err.code,
message = string.format("Failed to start `%s`, error: %s", bin, err),
}
return nil, e
elseif windows then
child:write_all(table.concat(paths, "\n"))
child:flush()
ya.drop(child:take_stdin())
end
return child
end
function M.placeholder(err, urls, paths)
if err.kind ~= "NotFound" then
return
end
local updates = {}
for i = 1, #paths do
updates[urls[i] or paths[i]] = "null/file1-not-found"
end
ya.emit("update_mimes", { updates = updates })
end
return M

View file

@ -0,0 +1,22 @@
local M = {}
function M:peek(job)
local err
if job.mime == "null/file1-not-found" then
err = string.format(
"Cannot find `%s` to detect the file's MIME type. Make sure it's installed and restart Yazi",
require("mime.local").file1_bin()
)
else
err = "Unknown error occurred while detecting MIME type"
end
local line = ui.Line(err):reverse()
ya.preview_widget(job, ui.Text(line):area(job.area):wrap(ui.Wrap.YES))
end
function M:seek() end
function M:spot(job) require("file"):spot(job) end
return M

View file

@ -43,6 +43,7 @@ impl Default for Loader {
("mime.local".to_owned(), preset!("plugins/mime-local").into()),
("mime.remote".to_owned(), preset!("plugins/mime-remote").into()),
("noop".to_owned(), preset!("plugins/noop").into()),
("null".to_owned(), preset!("plugins/null").into()),
("pdf".to_owned(), preset!("plugins/pdf").into()),
("session".to_owned(), preset!("plugins/session").into()),
("svg".to_owned(), preset!("plugins/svg").into()),