feat: custom styles for plugins (#3934)

This commit is contained in:
三咲雅 misaki masa 2026-05-05 06:03:25 +08:00 committed by GitHub
parent 5ad1e003f2
commit 10fc76db07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 522 additions and 84 deletions

View file

@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New `app:theme` action that hot-reload user themes/flavors ([#3906])
- Dynamic open/opener Lua API ([#3901])
- Dynamic previewer Lua API ([#3891])
- Custom styles for plugins ([#3934])
- Vim-like `lua` action that runs an inline Lua snippet ([#3813])
- Certificate authentication for SFTP VFS provider ([#3716])
- New `hovered` condition specifying different icons for hovered files ([#3728])
@ -1709,3 +1710,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3894]: https://github.com/sxyazi/yazi/pull/3894
[#3901]: https://github.com/sxyazi/yazi/pull/3901
[#3906]: https://github.com/sxyazi/yazi/pull/3906
[#3934]: https://github.com/sxyazi/yazi/pull/3934

70
Cargo.lock generated
View file

@ -1044,7 +1044,7 @@ dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.11.2",
"digest 0.11.3",
"fiat-crypto",
"rustc_version",
"subtle",
@ -1262,9 +1262,9 @@ dependencies = [
[[package]]
name = "digest"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.0",
"const-oid 0.10.2",
@ -1315,7 +1315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829"
dependencies = [
"der",
"digest 0.11.2",
"digest 0.11.3",
"elliptic-curve",
"rfc6979",
"signature",
@ -1364,7 +1364,7 @@ dependencies = [
"base16ct",
"crypto-bigint",
"crypto-common 0.2.1",
"digest 0.11.2",
"digest 0.11.3",
"hkdf",
"hybrid-array",
"once_cell",
@ -1896,7 +1896,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
dependencies = [
"digest 0.11.2",
"digest 0.11.3",
]
[[package]]
@ -2209,11 +2209,11 @@ dependencies = [
[[package]]
name = "kqueue-sys"
version = "1.0.4"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.11.1",
"libc",
]
@ -3002,7 +3002,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
dependencies = [
"digest 0.11.2",
"digest 0.11.3",
"hmac 0.13.0",
]
@ -3323,18 +3323,18 @@ dependencies = [
[[package]]
name = "profiling"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb"
dependencies = [
"quote",
"syn 2.0.117",
@ -3379,9 +3379,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.39.2"
version = "0.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b"
dependencies = [
"memchr",
]
@ -3769,7 +3769,7 @@ dependencies = [
"const-oid 0.10.2",
"crypto-bigint",
"crypto-primes",
"digest 0.11.2",
"digest 0.11.3",
"pkcs1",
"pkcs8",
"rand_core 0.10.1",
@ -4118,9 +4118,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.18.0"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f"
checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19"
dependencies = [
"base64",
"chrono",
@ -4137,9 +4137,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.18.0"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334"
dependencies = [
"darling 0.23.0",
"proc-macro2",
@ -4149,9 +4149,9 @@ dependencies = [
[[package]]
name = "serdect"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06"
checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e"
dependencies = [
"base16ct",
"serde",
@ -4176,7 +4176,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
"digest 0.11.3",
]
[[package]]
@ -4198,7 +4198,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
"digest 0.11.3",
]
[[package]]
@ -4207,7 +4207,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
dependencies = [
"digest 0.11.2",
"digest 0.11.3",
"keccak",
]
@ -4281,11 +4281,11 @@ dependencies = [
[[package]]
name = "signature"
version = "3.0.0-rc.10"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3"
checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
dependencies = [
"digest 0.11.2",
"digest 0.11.3",
"rand_core 0.10.1",
]
@ -4312,9 +4312,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]]
name = "slab"
@ -4696,9 +4696,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.52.1"
version = "1.52.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
dependencies = [
"bytes",
"libc",
@ -4861,9 +4861,9 @@ dependencies = [
[[package]]
name = "trash"
version = "5.2.5"
version = "5.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9b93a14fcf658568eb11b3ac4cb406822e916e2c55cdebc421beeb0bd7c94d8"
checksum = "7602e0c7d66ec2d92a8c917219fbc7894039efa2063b9064260110828a356f46"
dependencies = [
"chrono",
"libc",

View file

@ -67,11 +67,11 @@ russh = { version = "0.60.2", default-features = false, features =
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.149"
serde_with = "3.18.0"
serde_with = "3.19.0"
strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18"
tokio = { version = "1.52.1", features = [ "full" ] }
tokio = { version = "1.52.2", features = [ "full" ] }
tokio-stream = "0.1.18"
tokio-util = "0.7.18"
toml = { version = "1.1.2" }

View file

@ -1,5 +1,5 @@
mod macros;
yazi_macro::mod_pub!(config elements process);
yazi_macro::mod_pub!(config elements process theme);
yazi_macro::mod_flat!(access calculator cha chan chord_cow composer error fd file handle icon id image input iter layer mouse path permit range runtime scheme selector stage style url utils);

View file

@ -0,0 +1,18 @@
use mlua::IntoLua;
use crate::Style;
pub struct CustomField(yazi_config::theme::CustomField);
impl CustomField {
pub fn new(inner: impl Into<yazi_config::theme::CustomField>) -> Self { Self(inner.into()) }
}
impl IntoLua for CustomField {
fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
match self.0 {
yazi_config::theme::CustomField::Style(style) => Style::from(style).into_lua(lua),
yazi_config::theme::CustomField::String(s) => s.into_lua(lua),
}
}
}

View file

@ -0,0 +1,28 @@
use std::sync::Arc;
use hashbrown::HashMap;
use mlua::{IntoLua, MetaMethod, UserData, UserDataMethods, Value};
use yazi_shared::SnakeCasedString;
use crate::theme::CustomField;
pub struct CustomSection {
inner: Arc<HashMap<SnakeCasedString, yazi_config::theme::CustomField>>,
}
impl CustomSection {
pub fn new(inner: Arc<HashMap<SnakeCasedString, yazi_config::theme::CustomField>>) -> Self {
Self { inner }
}
}
impl UserData for CustomSection {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::Index, |lua, me, key: mlua::String| {
match me.inner.get(&*key.to_str()?) {
Some(value) => CustomField::new(value).into_lua(lua),
None => Ok(Value::Nil),
}
});
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(custom_field custom_section);

View file

@ -165,7 +165,7 @@ impl Actions {
fn file1_output() -> String {
use std::io::Write;
let p = Xdg::temp_dir().join(format!(".debug-{}.tmp", timestamp_us()));
let p = env::temp_dir().join(format!(".yazi-debug-{}.tmp", timestamp_us()));
std::fs::File::create_new(&p).map(|mut f| f.write_all(b"Hello, World!")).ok();
let program = env::var_os("YAZI_FILE_ONE").unwrap_or("file".into());

View file

@ -24,22 +24,38 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
let visitor_generics = generics_with_de(&generics);
let (impl_visitor_generics, ..) = visitor_generics.split_for_impl();
let fields: Vec<_> = named_fields(data).into_iter().map(|f| f.ident.unwrap()).collect();
let match_arms = fields.iter().map(|f| {
let name = ident_name(f);
quote! { #name => self.0.#f = map.next_value_seed(DeserializeOverSeed(self.0.#f))?, }
let (flatten_fields, normal_fields): (Vec<_>, Vec<_>) =
named_fields(data).into_iter().partition(|f| has_serde_attr(&f.attrs, "flatten"));
let field_hooks: Vec<_> = flatten_fields
.iter()
.chain(&normal_fields)
.map(|f| {
let ident = f.ident.as_ref().unwrap();
quote! { #ident: deserialized.#ident.deserialize_over_hook().map_err(Error::custom)? }
})
.collect();
let normal_arms = normal_fields.into_iter().map(|f| {
let ident = f.ident.unwrap();
let name = ident_name(&ident);
quote! { #name => self.0.#ident = map.next_value_seed(DeserializeOverSeed(self.0.#ident))? }
});
let hook_fields = fields.iter().map(|f| {
quote! { #f: deserialized.#f.deserialize_over_hook().map_err(Error::custom)? }
});
let flatten_arm = match flatten_fields.into_iter().next() {
Some(f) => {
let ident = f.ident.unwrap();
quote! { _ => self.0.#ident = self.0.#ident.deserialize_over_with(single_map_entry(key, &mut map))? }
}
None => quote! { _ => _ = map.next_value::<IgnoredAny>()? },
};
quote! {
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result<Self, __D::Error> {
use std::borrow::Cow;
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverSeed};
use yazi_shared::KebabCasedString;
use yazi_shim::{serde::single_map_entry, toml::{DeserializeOverHook, DeserializeOverSeed, DeserializeOverWith}};
struct V #impl_generics (#ident #ty_generics) #where_clause;
@ -51,10 +67,10 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
}
fn visit_map<__M: MapAccess<'__de>>(mut self, mut map: __M) -> Result<Self::Value, __M::Error> {
while let Some(key) = map.next_key::<Cow<str>>()? {
while let Some(key) = map.next_key::<KebabCasedString>()? {
match key.as_ref() {
#(#match_arms)*
_ => { map.next_value::<IgnoredAny>()?; }
#(#normal_arms,)*
#flatten_arm
}
}
Ok(self.0)
@ -62,7 +78,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
}
let deserialized = de.deserialize_map(V(self))?;
Ok(Self { #(#hook_fields,)* })
Ok(Self { #(#field_hooks,)* })
}
}
}
@ -77,7 +93,8 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
let visitor_generics = generics_with_de(&generics);
let (impl_visitor_generics, ..) = visitor_generics.split_for_impl();
let (mut match_arms, mut post_fields) = (vec![], vec![]);
let mut normal_arms = vec![];
let mut flatten_arm = quote! { _ => _ = map.next_value::<IgnoredAny>()? };
for field in named_fields(data) {
let (field_ident, field_ty) = (field.ident, field.ty);
let field_name = ident_name(field_ident.as_ref().unwrap());
@ -86,25 +103,16 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
continue;
}
if field_name == "selector" && has_serde_attr(&field.attrs, "flatten") {
match_arms.push(quote! {
"url" => selector_url = Some(map.next_value()?),
"mime" => selector_mime = Some(map.next_value()?),
});
post_fields.push(quote! {
self.0.#field_ident = Selector::new(
selector_url.or(self.0.#field_ident.url),
selector_mime.or(self.0.#field_ident.mime)
).map_err(Error::custom)?;
});
if has_serde_attr(&field.attrs, "flatten") {
flatten_arm = quote! { _ => self.0.#field_ident = self.0.#field_ident.deserialize_over_with(single_map_entry(key, &mut map))? };
continue;
}
let serde_attrs: Vec<_> = field.attrs.iter().filter(|a| a.path().is_ident("serde")).collect();
if serde_attrs.is_empty() {
match_arms.push(quote! { #field_name => self.0.#field_ident = map.next_value()?, });
normal_arms.push(quote! { #field_name => self.0.#field_ident = map.next_value()? });
} else {
match_arms.push(quote! {
normal_arms.push(quote! {
#field_name => {
#[derive(serde::Deserialize)]
struct H #impl_generics(#(#serde_attrs)* #field_ty,) #where_clause;
@ -117,9 +125,9 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
quote! {
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result<Self, __D::Error> {
use std::borrow::Cow;
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use crate::Selector;
use std::borrow::Cow;
use yazi_shim::{serde::single_map_entry, toml::DeserializeOverWith};
struct V #impl_generics (#ident #ty_generics) #where_clause;
@ -131,17 +139,13 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
}
fn visit_map<__M: MapAccess<'__de>>(mut self, mut map: __M) -> Result<Self::Value, __M::Error> {
let mut selector_url: Option<crate::Pattern> = None;
let mut selector_mime: Option<crate::Pattern> = None;
while let Some(key) = map.next_key::<Cow<str>>()? {
match key.as_ref() {
#(#match_arms)*
_ => { map.next_value::<IgnoredAny>()?; }
#(#normal_arms,)*
#flatten_arm
}
}
#(#post_fields)*
Ok(self.0)
}
}

View file

@ -71,9 +71,15 @@ pub struct OpenerRulesMatcher {
impl From<&Opener> for OpenerRulesMatcher {
fn from(opener: &Opener) -> Self {
let opener = opener.load_full();
let iter: hash_map::Iter<String, Arc<OpenerRules>> = opener.iter();
Self { iter: unsafe { mem::transmute(iter) }, _opener: opener }
let iter = unsafe {
mem::transmute::<
hash_map::Iter<'_, String, Arc<OpenerRules>>,
hash_map::Iter<'static, String, Arc<OpenerRules>>,
>(opener.iter())
};
Self { iter, _opener: opener }
}
}

View file

@ -1,5 +1,6 @@
use anyhow::{Result, ensure};
use serde::{Deserialize, Deserializer, de};
use yazi_shim::toml::DeserializeOverWith;
use crate::{Mixable, Pattern, Selectable};
@ -22,6 +23,16 @@ impl<'de> Deserialize<'de> for Selector {
}
}
impl DeserializeOverWith for Selector {
fn deserialize_over_with<'de, D: Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Self, D::Error> {
let new = Selector::deserialize(deserializer)?;
Self::new(new.url.or(self.url), new.mime.or(self.mime)).map_err(de::Error::custom)
}
}
impl Selector {
pub fn new(url: Option<Pattern>, mime: Option<Pattern>) -> Result<Self> {
ensure!(url.is_some() || mime.is_some(), "at least one of `url` or `mime` must be specified");

View file

@ -0,0 +1,85 @@
use std::{fmt, mem, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::{HashMap, hash_map};
use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}};
use yazi_codegen::{DeserializeOver, Overlay};
use yazi_shared::{KebabCasedString, SnakeCasedString};
use yazi_shim::{arc_swap::IntoPointee, toml::DeserializeOverWith};
use crate::theme::CustomSection;
#[derive(Debug, Default, DeserializeOver, Overlay)]
pub struct Custom(ArcSwap<HashMap<SnakeCasedString, CustomSection>>);
impl Deref for Custom {
type Target = ArcSwap<HashMap<SnakeCasedString, CustomSection>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<HashMap<SnakeCasedString, CustomSection>> for Custom {
fn from(value: HashMap<SnakeCasedString, CustomSection>) -> Self { Self(value.into_pointee()) }
}
impl Custom {
pub(super) fn unwrap_unchecked(self) -> HashMap<SnakeCasedString, CustomSection> {
Arc::try_unwrap(self.0.into_inner()).expect("unique custom arc")
}
}
impl<'de> Deserialize<'de> for Custom {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = HashMap<SnakeCasedString, CustomSection>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("a map") }
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut sections = HashMap::with_capacity(map.size_hint().unwrap_or(0));
while let Some(key) = map.next_key::<KebabCasedString>()? {
let section = map.next_value::<CustomSection>()?;
if !section.load().is_empty() {
sections.insert(key.into_snake_cased(), section);
}
}
Ok(sections)
}
}
Ok(de.deserialize_map(V)?.into())
}
}
impl DeserializeOverWith for Custom {
fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result<Self, D::Error> {
struct V(Custom);
impl<'de> Visitor<'de> for V {
type Value = Custom;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("a map") }
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Custom, M::Error> {
let mut sections = self.0.unwrap_unchecked();
while let Some(key) = map.next_key::<KebabCasedString>()? {
let (key, new) = (key.into_snake_cased(), map.next_value::<CustomSection>()?);
match sections.entry(key) {
hash_map::Entry::Occupied(mut oe) => {
let mut old = mem::take(oe.get_mut()).unwrap_unchecked();
old.extend(new.unwrap_unchecked());
oe.insert(old.into());
}
hash_map::Entry::Vacant(_) if new.load().is_empty() => {}
hash_map::Entry::Vacant(ve) => _ = ve.insert(new),
}
}
Ok(sections.into())
}
}
de.deserialize_map(V(self))
}
}

View file

@ -0,0 +1,12 @@
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub enum CustomField {
Style(crate::Style),
String(String),
}
impl From<&CustomField> for CustomField {
fn from(value: &CustomField) -> Self { value.clone() }
}

View file

@ -0,0 +1,90 @@
use std::{ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, de::{self, MapAccess, SeqAccess, Visitor}};
use yazi_codegen::{DeserializeOver, Overlay};
use yazi_shim::arc_swap::IntoPointee;
use yazi_shared::SnakeCasedString;
use crate::theme::CustomField;
#[derive(Debug, Default, DeserializeOver, Overlay)]
pub struct CustomSection(ArcSwap<HashMap<SnakeCasedString, CustomField>>);
impl Deref for CustomSection {
type Target = ArcSwap<HashMap<SnakeCasedString, CustomField>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<HashMap<SnakeCasedString, CustomField>> for CustomSection {
fn from(value: HashMap<SnakeCasedString, CustomField>) -> Self { Self(value.into_pointee()) }
}
impl CustomSection {
pub(super) fn unwrap_unchecked(self) -> HashMap<SnakeCasedString, CustomField> {
Arc::try_unwrap(self.0.into_inner()).expect("unique custom section arc")
}
}
impl<'de> Deserialize<'de> for CustomSection {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = CustomSection;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a style section table or a skippable scalar")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut fields = HashMap::with_capacity(map.size_hint().unwrap_or(0));
while let Some(k) = map.next_key::<SnakeCasedString>()? {
fields.insert(k, map.next_value::<CustomField>()?);
}
Ok(CustomSection(fields.into_pointee()))
}
fn visit_bool<E: de::Error>(self, _: bool) -> Result<Self::Value, E> {
Ok(CustomSection::default())
}
fn visit_i64<E: de::Error>(self, _: i64) -> Result<Self::Value, E> {
Ok(CustomSection::default())
}
fn visit_u64<E: de::Error>(self, _: u64) -> Result<Self::Value, E> {
Ok(CustomSection::default())
}
fn visit_f64<E: de::Error>(self, _: f64) -> Result<Self::Value, E> {
Ok(CustomSection::default())
}
fn visit_str<E: de::Error>(self, _: &str) -> Result<Self::Value, E> {
Ok(CustomSection::default())
}
fn visit_bytes<E: de::Error>(self, _: &[u8]) -> Result<Self::Value, E> {
Ok(CustomSection::default())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> { Ok(CustomSection::default()) }
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> { Ok(CustomSection::default()) }
fn visit_some<D2: Deserializer<'de>>(self, de: D2) -> Result<Self::Value, D2::Error> {
CustomSection::deserialize(de)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
while seq.next_element::<de::IgnoredAny>()?.is_some() {}
Ok(CustomSection::default())
}
}
de.deserialize_any(V)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(filetype filetype_rule filetype_rules flavor icon icon_cond icon_conds icon_glob icon_globs icon_names is theme);
yazi_macro::mod_flat!(custom custom_field custom_section filetype filetype_rule filetype_rules flavor icon icon_cond icon_conds icon_glob icon_globs icon_names is theme);

View file

@ -7,7 +7,7 @@ use yazi_codegen::{DeserializeOver, DeserializeOver1, DeserializeOver2, Overlay}
use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shim::{arc_swap::IntoPointee, cell::SyncCell};
use super::{Filetype, Flavor, Icon};
use super::{Custom, Filetype, Flavor, Icon};
use crate::{Style, normalize_path};
#[derive(Deserialize, DeserializeOver, DeserializeOver1, Overlay)]
@ -32,6 +32,10 @@ pub struct Theme {
// File-specific styles
pub filetype: Filetype,
pub icon: Icon,
// User-defined custom sections
#[serde(flatten, default)]
pub custom: Custom,
}
#[derive(Deserialize, DeserializeOver, DeserializeOver2, Overlay)]

View file

@ -49,4 +49,4 @@ core-foundation-sys = { workspace = true }
objc2 = { workspace = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
trash = "5.2.5"
trash = "5.2.6"

View file

@ -1,5 +1,5 @@
use mlua::{IntoLua, Lua, Value};
use yazi_binding::{Composer, ComposerGet, ComposerSet, Style, Url};
use yazi_binding::{Composer, ComposerGet, ComposerSet, Style, Url, theme::CustomSection};
use yazi_config::THEME;
use crate::LUA;
@ -22,7 +22,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"cmp" => cmp(),
b"tasks" => tasks(),
b"help" => help(),
_ => return Ok(Value::Nil),
_ => return custom(lua, key),
}
.into_lua(lua)
}
@ -375,3 +375,10 @@ fn help() -> Composer<ComposerGet, ComposerSet> {
Composer::new(get, set)
}
fn custom(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
match THEME.custom.load().get(str::from_utf8(key)?) {
Some(section) => CustomSection::new(section.load_full()).into_lua(lua),
None => Ok(Value::Nil),
}
}

View file

@ -11,6 +11,8 @@ pub trait BytesExt {
fn rsplit_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>;
fn snake_cased(&self) -> bool;
fn split_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>;
}
@ -47,6 +49,10 @@ impl BytesExt for [u8] {
None
}
fn snake_cased(&self) -> bool {
self.iter().all(|&b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'_'))
}
fn split_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> {
let idx = memchr::memmem::find(self, sep)?;
let (a, b) = self.split_at(idx);

View file

@ -0,0 +1,71 @@
use std::{borrow::{Borrow, Cow}, ffi::OsStr, fmt::{Display, Formatter}, ops::Deref};
use serde::{Deserialize, Deserializer, Serialize};
use crate::{BytesExt, SnakeCasedString};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct KebabCasedString(String);
impl KebabCasedString {
pub fn new(s: String) -> Option<Self> { s.as_bytes().kebab_cased().then_some(Self(s)) }
pub fn into_snake_cased(self) -> SnakeCasedString {
let mut b = self.0.into_bytes();
b.iter_mut().for_each(|c| {
if *c == b'-' {
*c = b'_'
}
});
SnakeCasedString(unsafe { String::from_utf8_unchecked(b) })
}
}
impl Deref for KebabCasedString {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Borrow<str> for KebabCasedString {
#[inline]
fn borrow(&self) -> &str { &self.0 }
}
impl Borrow<String> for KebabCasedString {
#[inline]
fn borrow(&self) -> &String { &self.0 }
}
impl AsRef<str> for KebabCasedString {
#[inline]
fn as_ref(&self) -> &str { &self.0 }
}
impl AsRef<OsStr> for KebabCasedString {
#[inline]
fn as_ref(&self) -> &OsStr { self.0.as_ref() }
}
impl Display for KebabCasedString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.0, f) }
}
impl From<KebabCasedString> for String {
#[inline]
fn from(value: KebabCasedString) -> Self { value.0 }
}
impl From<KebabCasedString> for Cow<'_, str> {
#[inline]
fn from(value: KebabCasedString) -> Self { Cow::Owned(value.0) }
}
impl<'de> Deserialize<'de> for KebabCasedString {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a kebab-cased string"))
}
}

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(data event loc path pool scheme shell strand translit url wtf8);
yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id last_value layer localset natsort non_empty_string os predictor source terminal tests throttle time utf8);
yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id kebab_cased_string last_value layer localset natsort non_empty_string os predictor snake_cased_string source terminal tests throttle time utf8);
pub fn init() {
LOCAL_SET.with(tokio::task::LocalSet::new);

View file

@ -57,6 +57,6 @@ impl From<NonEmptyString> for OsString {
impl<'de> Deserialize<'de> for NonEmptyString {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(value).ok_or_else(|| serde::de::Error::custom("string cannot be empty"))
Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a non-empty string"))
}
}

View file

@ -0,0 +1,56 @@
use std::{borrow::Borrow, ffi::OsStr, fmt::{Display, Formatter}, ops::Deref};
use serde::{Deserialize, Deserializer, Serialize};
use crate::BytesExt;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct SnakeCasedString(pub(super) String);
impl SnakeCasedString {
pub fn new(s: String) -> Option<Self> { s.as_bytes().snake_cased().then_some(Self(s)) }
}
impl Deref for SnakeCasedString {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Borrow<str> for SnakeCasedString {
#[inline]
fn borrow(&self) -> &str { &self.0 }
}
impl Borrow<String> for SnakeCasedString {
#[inline]
fn borrow(&self) -> &String { &self.0 }
}
impl AsRef<str> for SnakeCasedString {
#[inline]
fn as_ref(&self) -> &str { &self.0 }
}
impl AsRef<OsStr> for SnakeCasedString {
#[inline]
fn as_ref(&self) -> &OsStr { self.0.as_ref() }
}
impl Display for SnakeCasedString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.0, f) }
}
impl From<SnakeCasedString> for String {
#[inline]
fn from(value: SnakeCasedString) -> Self { value.0 }
}
impl<'de> Deserialize<'de> for SnakeCasedString {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a snake-cased string"))
}
}

View file

@ -0,0 +1,37 @@
use std::borrow::Cow;
use serde::{Deserializer, de::{DeserializeSeed, IntoDeserializer, MapAccess, value::MapAccessDeserializer}};
struct SingleMapEntryAccess<'k, 'a, M> {
key: Option<Cow<'k, str>>,
map: &'a mut M,
}
impl<'k, 'a, 'de, M: MapAccess<'de>> MapAccess<'de> for SingleMapEntryAccess<'k, 'a, M> {
type Error = M::Error;
fn next_key_seed<K: DeserializeSeed<'de>>(
&mut self,
seed: K,
) -> Result<Option<K::Value>, Self::Error> {
match self.key.take() {
Some(k) => seed.deserialize(k.into_deserializer()).map(Some),
None => Ok(None),
}
}
fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value, Self::Error> {
self.map.next_value_seed(seed)
}
}
pub fn single_map_entry<'k, 'a, 'de, K, M>(
key: K,
map: &'a mut M,
) -> impl Deserializer<'de, Error = M::Error> + use<'k, 'a, 'de, K, M>
where
K: Into<Cow<'k, str>>,
M: MapAccess<'de>,
{
MapAccessDeserializer::new(SingleMapEntryAccess { key: Some(key.into()), map })
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(traits);
yazi_macro::mod_flat!(map traits);