diff --git a/yazi-binding/src/error.rs b/yazi-binding/src/error.rs index eef599a3..e53698ae 100644 --- a/yazi-binding/src/error.rs +++ b/yazi-binding/src/error.rs @@ -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::("kind")?.to_str()?, + t.raw_get("code")?, + &t.raw_get::("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) -> 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>(methods: &mut M) { diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 038efea9..b74651cd 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -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" }, ] diff --git a/yazi-fs/src/error/error.rs b/yazi-fs/src/error/error.rs index 161636c4..a017ae54 100644 --- a/yazi-fs/src/error/error.rs +++ b/yazi-fs/src/error/error.rs @@ -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, message: &str) -> Result { + 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 { match self { Self::Kind(_) => None, diff --git a/yazi-fs/src/error/serde.rs b/yazi-fs/src/error/serde.rs index a5b9a289..1bc6b4a6 100644 --- a/yazi-fs/src/error/serde.rs +++ b/yazi-fs/src/error/serde.rs @@ -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 { 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)?) } } }) diff --git a/yazi-plugin/preset/plugins/mime-local.lua b/yazi-plugin/preset/plugins/mime-local.lua index fb50e2d2..1e2cfdcb 100644 --- a/yazi-plugin/preset/plugins/mime-local.lua +++ b/yazi-plugin/preset/plugins/mime-local.lua @@ -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 diff --git a/yazi-plugin/preset/plugins/null.lua b/yazi-plugin/preset/plugins/null.lua new file mode 100644 index 00000000..279c9038 --- /dev/null +++ b/yazi-plugin/preset/plugins/null.lua @@ -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 diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index d5fff9bd..4185718f 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -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()),