feat: add a next property to the preloader rules to allow running multiple preloaders (#1058)

This commit is contained in:
三咲雅 · Misaki Masa 2024-05-19 18:07:36 +08:00 committed by sxyazi
parent 0ff4835f8d
commit c2affae3a9
No known key found for this signature in database
28 changed files with 219 additions and 189 deletions

9
Cargo.lock generated
View File

@ -120,9 +120,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.83"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arc-swap"
@ -1086,9 +1086,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.154"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libredox"
@ -2924,6 +2924,7 @@ dependencies = [
"ratatui",
"regex",
"serde",
"shell-words",
"tokio",
"tracing",
]

View File

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1713805509,
"narHash": "sha256-YgSEan4CcrjivCNO5ZNzhg7/8ViLkZ4CB/GrGBVSudo=",
"lastModified": 1716097317,
"narHash": "sha256-1UMrLtgzielG/Sop6gl6oTSM4pDt7rF9j9VuxhDWDlY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1e1dc66fe68972a76679644a5577828b6a7e8be4",
"rev": "8535fb92661f37ff9f0da3007fbc942f7d134b41",
"type": "github"
},
"original": {
@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1713924823,
"narHash": "sha256-kOeyS3GFwgnKvzuBMmFqEAX0xwZ7Nj4/5tXuvpZ0d4U=",
"lastModified": 1716085073,
"narHash": "sha256-3+9gI93XxszWA2+9S2xZfws1QArPX/MC6nahOGpcMB4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8a2edac3ae926a2a6ce60f4595dcc4540fc8cad4",
"rev": "cfc8776011bd83508324115d353222475e1601c0",
"type": "github"
},
"original": {

View File

@ -13,7 +13,7 @@ yazi-config = { path = "../yazi-config", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
arc-swap = "1.7.1"
base64 = "0.22.1"
color_quant = "1.1.0"

View File

@ -13,7 +13,7 @@ yazi-dds = { path = "../yazi-dds", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
clap = { version = "4.5.4", features = [ "derive" ] }
crossterm = "0.27.0"
md-5 = "0.10.6"
@ -22,7 +22,7 @@ tokio = { version = "1.37.0", features = [ "full" ] }
toml_edit = "0.22.13"
[build-dependencies]
anyhow = "1.0.83"
anyhow = "1.0.86"
clap = { version = "4.5.4", features = [ "derive" ] }
clap_complete = "4.5.2"
clap_complete_fig = "4.5.0"

View File

@ -12,7 +12,7 @@ repository = "https://github.com/sxyazi/yazi"
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
arc-swap = "1.7.1"
crossterm = "0.27.0"
globset = "0.4.14"

View File

@ -80,12 +80,12 @@ suppress_preload = false
[plugin]
preloaders = [
{ name = "*", cond = "!mime", run = "mime", multi = true, prio = "high" },
{ name = "*", cond = "!mime", run = "mime", next = true, multi = true, prio = "high" },
# Image
{ mime = "image/svg+xml", run = "magick" },
{ mime = "image/heic", run = "magick" },
{ mime = "image/jxl", run = "magick" },
{ mime = "image/*", run = "image" },
{ mime = "image/svg+xml", run = "magick" },
{ mime = "image/heic", run = "magick" },
{ mime = "image/jxl", run = "magick" },
{ mime = "image/*", run = "image" },
# Video
{ mime = "video/*", run = "video" },
# PDF
@ -102,10 +102,10 @@ previewers = [
# JSON
{ mime = "application/json", run = "json" },
# Image
{ mime = "image/svg+xml", run = "magick" },
{ mime = "image/heic", run = "magick" },
{ mime = "image/jxl", run = "magick" },
{ mime = "image/*", run = "image" },
{ mime = "image/svg+xml", run = "magick" },
{ mime = "image/heic", run = "magick" },
{ mime = "image/jxl", run = "magick" },
{ mime = "image/*", run = "image" },
# Video
{ mime = "video/*", run = "video" },
# PDF

View File

@ -23,10 +23,10 @@ impl Open {
P: AsRef<Path>,
M: AsRef<str>,
{
let is_folder = mime.as_ref() == MIME_DIR;
let is_dir = mime.as_ref() == MIME_DIR;
self.rules.iter().find_map(|rule| {
if rule.mime.as_ref().is_some_and(|p| p.match_mime(&mime))
|| rule.name.as_ref().is_some_and(|p| p.match_path(&path, is_folder))
|| rule.name.as_ref().is_some_and(|p| p.match_path(&path, is_dir))
{
let openers = rule
.use_

View File

@ -6,9 +6,9 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(try_from = "String")]
pub struct Pattern {
inner: globset::GlobMatcher,
is_star: bool,
is_folder: bool,
inner: globset::GlobMatcher,
is_dir: bool,
is_star: bool,
}
impl Pattern {
@ -16,15 +16,15 @@ impl Pattern {
pub fn match_mime(&self, str: impl AsRef<str>) -> bool { self.inner.is_match(str.as_ref()) }
#[inline]
pub fn match_path(&self, path: impl AsRef<Path>, is_folder: bool) -> bool {
is_folder == self.is_folder && (self.is_star || self.inner.is_match(path))
pub fn match_path(&self, path: impl AsRef<Path>, is_dir: bool) -> bool {
is_dir == self.is_dir && (self.is_star || self.inner.is_match(path))
}
#[inline]
pub fn any_file(&self) -> bool { self.is_star && !self.is_folder }
pub fn any_file(&self) -> bool { self.is_star && !self.is_dir }
#[inline]
pub fn any_dir(&self) -> bool { self.is_star && self.is_folder }
pub fn any_dir(&self) -> bool { self.is_star && self.is_dir }
}
impl TryFrom<&str> for Pattern {
@ -42,7 +42,7 @@ impl TryFrom<&str> for Pattern {
.build()?
.compile_matcher();
Ok(Self { inner, is_star: b == "*", is_folder: b.len() < a.len() })
Ok(Self { inner, is_dir: b.len() < a.len(), is_star: b == "*" })
}
}

View File

@ -1,12 +1,9 @@
mod plugin;
mod props;
mod rule;
mod run;
mod preloader;
mod previewer;
pub use plugin::*;
pub use props::*;
pub use rule::*;
#[allow(unused_imports)]
pub use run::*;
pub use preloader::*;
pub use previewer::*;
pub const MAX_PRELOADERS: u8 = 32;

View File

@ -3,13 +3,13 @@ use std::path::Path;
use serde::Deserialize;
use yazi_shared::MIME_DIR;
use super::PluginRule;
use super::{Preloader, Previewer};
use crate::{plugin::MAX_PRELOADERS, Preset, MERGED_YAZI};
#[derive(Deserialize)]
pub struct Plugin {
pub preloaders: Vec<PluginRule>,
pub previewers: Vec<PluginRule>,
pub preloaders: Vec<Preloader>,
pub previewers: Vec<Previewer>,
}
impl Default for Plugin {
@ -21,17 +21,17 @@ impl Default for Plugin {
#[derive(Deserialize)]
struct Shadow {
preloaders: Vec<PluginRule>,
preloaders: Vec<Preloader>,
#[serde(default)]
prepend_preloaders: Vec<PluginRule>,
prepend_preloaders: Vec<Preloader>,
#[serde(default)]
append_preloaders: Vec<PluginRule>,
append_preloaders: Vec<Preloader>,
previewers: Vec<PluginRule>,
previewers: Vec<Previewer>,
#[serde(default)]
prepend_previewers: Vec<PluginRule>,
prepend_previewers: Vec<Previewer>,
#[serde(default)]
append_previewers: Vec<PluginRule>,
append_previewers: Vec<Previewer>,
}
let mut shadow = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().plugin;
@ -50,9 +50,6 @@ impl Default for Plugin {
}
for (i, preloader) in shadow.preloaders.iter_mut().enumerate() {
if preloader.sync {
panic!("Preloaders cannot be synchronous");
}
preloader.id = i as u8;
}
@ -66,24 +63,34 @@ impl Plugin {
path: &Path,
mime: Option<&str>,
f: impl Fn(&str) -> bool + Copy,
) -> Vec<&PluginRule> {
let is_folder = mime == Some(MIME_DIR);
self
.preloaders
.iter()
.filter(|&rule| {
rule.cond.as_ref().and_then(|c| c.eval(f)) != Some(false)
&& (rule.mime.as_ref().zip(mime).map_or(false, |(p, m)| p.match_mime(m))
|| rule.name.as_ref().is_some_and(|p| p.match_path(path, is_folder)))
})
.collect()
) -> Vec<&Preloader> {
let is_dir = mime == Some(MIME_DIR);
let mut preloaders = Vec::with_capacity(1);
for p in &self.preloaders {
if p.cond.as_ref().and_then(|c| c.eval(f)) == Some(false) {
continue;
}
if !p.mime.as_ref().zip(mime).map_or(false, |(p, m)| p.match_mime(m))
&& !p.name.as_ref().is_some_and(|p| p.match_path(path, is_dir))
{
continue;
}
preloaders.push(p);
if !p.next {
break;
}
}
preloaders
}
pub fn previewer(&self, path: &Path, mime: &str) -> Option<&PluginRule> {
let is_folder = mime == MIME_DIR;
self.previewers.iter().find(|&rule| {
rule.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| rule.name.as_ref().is_some_and(|p| p.match_path(path, is_folder))
pub fn previewer(&self, path: &Path, mime: &str) -> Option<&Previewer> {
let is_dir = mime == MIME_DIR;
self.previewers.iter().find(|&p| {
p.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| p.name.as_ref().is_some_and(|p| p.match_path(path, is_dir))
})
}
}

View File

@ -0,0 +1,39 @@
use serde::Deserialize;
use yazi_shared::{event::Cmd, Condition};
use crate::{Pattern, Priority};
#[derive(Debug, Deserialize)]
pub struct Preloader {
#[serde(skip)]
pub id: u8,
pub cond: Option<Condition>,
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Cmd,
#[serde(default)]
pub next: bool,
#[serde(default)]
pub multi: bool,
#[serde(default)]
pub prio: Priority,
}
#[derive(Debug, Clone)]
pub struct PreloaderProps {
pub id: u8,
pub name: String,
pub multi: bool,
pub prio: Priority,
}
impl From<&Preloader> for PreloaderProps {
fn from(preloader: &Preloader) -> Self {
Self {
id: preloader.id,
name: preloader.run.name.to_owned(),
multi: preloader.multi,
prio: preloader.prio,
}
}
}

View File

@ -0,0 +1,21 @@
use serde::Deserialize;
use yazi_shared::event::Cmd;
use crate::Pattern;
#[derive(Debug, Deserialize)]
pub struct Previewer {
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Cmd,
#[serde(default)]
pub sync: bool,
}
impl Previewer {
#[inline]
pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) }
}

View File

@ -1,16 +0,0 @@
use super::PluginRule;
use crate::Priority;
#[derive(Debug, Clone)]
pub struct PluginProps {
pub id: u8,
pub name: String,
pub multi: bool,
pub prio: Priority,
}
impl From<&PluginRule> for PluginProps {
fn from(rule: &PluginRule) -> Self {
Self { id: rule.id, name: rule.run.name.to_owned(), multi: rule.multi, prio: rule.prio }
}
}

View File

@ -1,29 +0,0 @@
use serde::Deserialize;
use yazi_shared::{event::Cmd, Condition};
use crate::{Pattern, Priority};
#[derive(Debug, Deserialize)]
pub struct PluginRule {
#[serde(skip)]
pub id: u8,
pub cond: Option<Condition>,
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
#[serde(deserialize_with = "super::run_deserialize")]
pub run: Cmd,
#[serde(default)]
pub sync: bool,
#[serde(default)]
pub multi: bool,
#[serde(default)]
pub prio: Priority,
}
impl PluginRule {
#[inline]
pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) }
}

View File

@ -1,39 +0,0 @@
use std::fmt;
use anyhow::Result;
use serde::{de::{self, Visitor}, Deserializer};
use yazi_shared::event::Cmd;
pub(super) fn run_deserialize<'de, D>(deserializer: D) -> Result<Cmd, D::Error>
where
D: Deserializer<'de>,
{
struct RunVisitor;
impl<'de> Visitor<'de> for RunVisitor {
type Value = Cmd;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a `run` string or array of strings")
}
fn visit_seq<A>(self, _: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
Err(de::Error::custom("`run` within [plugin] must be a string"))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if value.is_empty() {
return Err(de::Error::custom("`run` within [plugin] cannot be empty"));
}
Ok(Cmd { name: value.to_owned(), ..Default::default() })
}
}
deserializer.deserialize_any(RunVisitor)
}

View File

@ -19,7 +19,7 @@ yazi-scheduler = { path = "../yazi-scheduler", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
bitflags = "2.5.0"
crossterm = "0.27.0"
futures = "0.3.30"
@ -39,4 +39,4 @@ shell-words = "1.1.0"
tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] }
[target."cfg(unix)".dependencies]
libc = "0.2.154"
libc = "0.2.155"

View File

@ -1,6 +1,6 @@
use std::{collections::HashMap, mem};
use yazi_config::{manager::SortBy, plugin::{PluginRule, MAX_PRELOADERS}, PLUGIN};
use yazi_config::{manager::SortBy, plugin::{Preloader, MAX_PRELOADERS}, PLUGIN};
use yazi_shared::{fs::{File, Url}, MIME_DIR};
use super::Tasks;
@ -34,15 +34,15 @@ impl Tasks {
drop(loaded);
let mut loaded = self.scheduler.preload.rule_loaded.write();
let mut go = |rule: &PluginRule, targets: Vec<&File>| {
let mut go = |preloader: &Preloader, targets: Vec<&File>| {
for &f in &targets {
if let Some(n) = loaded.get_mut(&f.url) {
*n |= 1 << rule.id;
*n |= 1 << preloader.id;
} else {
loaded.insert(f.url.clone(), 1 << rule.id);
loaded.insert(f.url.clone(), 1 << preloader.id);
}
}
self.scheduler.preload_paged(rule, targets);
self.scheduler.preload_paged(preloader, targets);
};
#[allow(clippy::needless_range_loop)]

View File

@ -17,7 +17,7 @@ yazi-boot = { path = "../yazi-boot", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
mlua = { version = "0.9.8", features = [ "lua54" ] }
parking_lot = "0.12.2"
serde = { version = "1.0.202", features = [ "derive" ] }

View File

@ -23,7 +23,7 @@ yazi-proxy = { path = "../yazi-proxy", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
better-panic = "0.3.0"
crossterm = { version = "0.27.0", features = [ "event-stream" ] }
fdlimit = "0.3.0"
@ -41,7 +41,7 @@ tracing-appender = "0.2.3"
tracing-subscriber = "0.3.18"
[target."cfg(unix)".dependencies]
libc = "0.2.154"
libc = "0.2.155"
signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ] }
[target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies]

View File

@ -22,7 +22,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
ansi-to-tui = "3.1.0"
anyhow = "1.0.83"
anyhow = "1.0.86"
base64 = "0.22.1"
crossterm = "0.27.0"
futures = "0.3.30"

View File

@ -50,10 +50,12 @@ end
local function entry()
local st = state()
if st.empty == true then
return fail("No directory history in the database, check out the `zoxide` docs to set it up.")
elseif st.empty == nil and head(st.cwd) < 2 then
set_state(true)
if st.empty == nil then
st.empty = head(st.cwd) < 2
set_state(st.empty)
end
if st.empty then
return fail("No directory history in the database, check out the `zoxide` docs to set it up.")
end

View File

@ -17,6 +17,6 @@ yazi-config = { path = "../yazi-config", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
mlua = { version = "0.9.8", features = [ "lua54" ] }
tokio = { version = "1.37.0", features = [ "full" ] }

View File

@ -16,7 +16,7 @@ yazi-proxy = { path = "../yazi-proxy", version = "0.2.5" }
yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.83"
anyhow = "1.0.86"
async-priority-channel = "0.2.0"
futures = "0.3.30"
parking_lot = "0.12.2"
@ -27,7 +27,7 @@ tokio = { version = "1.37.0", features = [ "full" ] }
tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] }
[target."cfg(unix)".dependencies]
libc = "0.2.154"
libc = "0.2.155"
[target.'cfg(not(target_os = "android"))'.dependencies]
trash = "4.1.1"

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use yazi_config::plugin::PluginProps;
use yazi_config::plugin::PreloaderProps;
use yazi_shared::{fs::Url, Throttle};
#[derive(Debug)]
@ -21,7 +21,7 @@ impl PreloadOp {
#[derive(Clone, Debug)]
pub struct PreloadOpRule {
pub id: usize,
pub plugin: PluginProps,
pub plugin: PreloaderProps,
pub targets: Vec<yazi_shared::fs::File>,
}

View File

@ -4,7 +4,7 @@ use anyhow::Result;
use futures::{future::BoxFuture, FutureExt};
use parking_lot::Mutex;
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, task::JoinHandle};
use yazi_config::{open::Opener, plugin::PluginRule, TASKS};
use yazi_config::{open::Opener, plugin::Preloader, TASKS};
use yazi_dds::Pump;
use yazi_shared::{event::Data, fs::{unique_path, Url}, Throttle};
@ -218,13 +218,13 @@ impl Scheduler {
self.plugin.macro_(PluginOpEntry { id, name, args }).ok();
}
pub fn preload_paged(&self, rule: &PluginRule, targets: Vec<&yazi_shared::fs::File>) {
pub fn preload_paged(&self, preloader: &Preloader, targets: Vec<&yazi_shared::fs::File>) {
let id = self.ongoing.lock().add(
TaskKind::Preload,
format!("Run preloader `{}` with {} target(s)", rule.run.name, targets.len()),
format!("Run preloader `{}` with {} target(s)", preloader.run.name, targets.len()),
);
let plugin = rule.into();
let plugin = preloader.into();
let targets = targets.into_iter().cloned().collect();
let preload = self.preload.clone();
_ = self.micro.try_send(

View File

@ -1,15 +1,16 @@
[package]
name = "yazi-shared"
version = "0.2.5"
edition = "2021"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi shared library"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
name = "yazi-shared"
version = "0.2.5"
edition = "2021"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi shared library"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
rust-version = "1.78.0"
[dependencies]
anyhow = "1.0.83"
anyhow = "1.0.86"
bitflags = "2.5.0"
crossterm = "0.27.0"
dirs = "5.0.1"
@ -20,10 +21,11 @@ percent-encoding = "2.3.1"
ratatui = "=0.26.1"
regex = "1.10.4"
serde = { version = "1.0.202", features = [ "derive" ] }
shell-words = "1.1.0"
tokio = { version = "1.37.0", features = [ "full" ] }
# Logging
tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] }
[target."cfg(unix)".dependencies]
libc = "0.2.154"
libc = "0.2.155"

View File

@ -1,4 +1,7 @@
use std::{any::Any, collections::HashMap, fmt::{self, Display}};
use std::{any::Any, collections::HashMap, fmt::{self, Display}, mem, str::FromStr};
use anyhow::bail;
use serde::{de, Deserialize};
use super::Data;
@ -112,3 +115,46 @@ impl Display for Cmd {
Ok(())
}
}
impl FromStr for Cmd {
type Err = anyhow::Error;
#[allow(clippy::explicit_counter_loop)]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut args = shell_words::split(s)?;
if args.is_empty() || args[0].is_empty() {
bail!("command name cannot be empty");
}
let mut cmd = Cmd { name: mem::take(&mut args[0]), ..Default::default() };
let mut i = 0usize;
for arg in args.into_iter().skip(1) {
let Some(arg) = arg.strip_prefix("--") else {
cmd.args.insert(i.to_string(), Data::String(arg));
i += 1;
continue;
};
let mut parts = arg.splitn(2, '=');
let Some(key) = parts.next().map(|s| s.to_owned()) else {
bail!("invalid argument: {arg}");
};
if let Some(val) = parts.next() {
cmd.args.insert(key, Data::String(val.to_owned()));
} else {
cmd.args.insert(key, Data::Boolean(true));
}
}
Ok(cmd)
}
}
impl<'de> Deserialize<'de> for Cmd {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
<_>::from_str(&String::deserialize(deserializer)?).map_err(de::Error::custom)
}
}

View File

@ -61,8 +61,7 @@ impl From<Metadata> for Cha {
kind: ck,
len: m.len(),
accessed: m.accessed().ok(),
// TODO: remove this once https://github.com/rust-lang/rust/issues/108277 is fixed.
created: None,
created: m.created().ok(),
modified: m.modified().ok(),
#[cfg(unix)]