diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a02f545..d19c1f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 7cffc020..fedef8b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 634f3ce6..885ce464 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/yazi-binding/src/lib.rs b/yazi-binding/src/lib.rs index 8dda12e8..b1cfd06e 100644 --- a/yazi-binding/src/lib.rs +++ b/yazi-binding/src/lib.rs @@ -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); diff --git a/yazi-binding/src/theme/custom_field.rs b/yazi-binding/src/theme/custom_field.rs new file mode 100644 index 00000000..760ad6b5 --- /dev/null +++ b/yazi-binding/src/theme/custom_field.rs @@ -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) -> Self { Self(inner.into()) } +} + +impl IntoLua for CustomField { + fn into_lua(self, lua: &mlua::Lua) -> mlua::Result { + 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), + } + } +} diff --git a/yazi-binding/src/theme/custom_section.rs b/yazi-binding/src/theme/custom_section.rs new file mode 100644 index 00000000..4404bec1 --- /dev/null +++ b/yazi-binding/src/theme/custom_section.rs @@ -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>, +} + +impl CustomSection { + pub fn new(inner: Arc>) -> Self { + Self { inner } + } +} + +impl UserData for CustomSection { + fn add_methods>(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), + } + }); + } +} diff --git a/yazi-binding/src/theme/mod.rs b/yazi-binding/src/theme/mod.rs new file mode 100644 index 00000000..ae58aae2 --- /dev/null +++ b/yazi-binding/src/theme/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(custom_field custom_section); diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index cc6c058f..66080b89 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -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()); diff --git a/yazi-codegen/src/lib.rs b/yazi-codegen/src/lib.rs index b63a4fad..e50e64ff 100644 --- a/yazi-codegen/src/lib.rs +++ b/yazi-codegen/src/lib.rs @@ -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::()? }, + }; 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 { - 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 { - while let Some(key) = map.next_key::>()? { + while let Some(key) = map.next_key::()? { match key.as_ref() { - #(#match_arms)* - _ => { map.next_value::()?; } + #(#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::()? }; 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 { - 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 { - let mut selector_url: Option = None; - let mut selector_mime: Option = None; - while let Some(key) = map.next_key::>()? { match key.as_ref() { - #(#match_arms)* - _ => { map.next_value::()?; } + #(#normal_arms,)* + #flatten_arm } } - #(#post_fields)* Ok(self.0) } } diff --git a/yazi-config/src/opener/rules.rs b/yazi-config/src/opener/rules.rs index 9849832f..f1221d3f 100644 --- a/yazi-config/src/opener/rules.rs +++ b/yazi-config/src/opener/rules.rs @@ -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> = opener.iter(); - Self { iter: unsafe { mem::transmute(iter) }, _opener: opener } + let iter = unsafe { + mem::transmute::< + hash_map::Iter<'_, String, Arc>, + hash_map::Iter<'static, String, Arc>, + >(opener.iter()) + }; + + Self { iter, _opener: opener } } } diff --git a/yazi-config/src/selector.rs b/yazi-config/src/selector.rs index cdbb8c7b..529df7a0 100644 --- a/yazi-config/src/selector.rs +++ b/yazi-config/src/selector.rs @@ -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 { + 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, mime: Option) -> Result { ensure!(url.is_some() || mime.is_some(), "at least one of `url` or `mime` must be specified"); diff --git a/yazi-config/src/theme/custom.rs b/yazi-config/src/theme/custom.rs new file mode 100644 index 00000000..c6e3fd29 --- /dev/null +++ b/yazi-config/src/theme/custom.rs @@ -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>); + +impl Deref for Custom { + type Target = ArcSwap>; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From> for Custom { + fn from(value: HashMap) -> Self { Self(value.into_pointee()) } +} + +impl Custom { + pub(super) fn unwrap_unchecked(self) -> HashMap { + Arc::try_unwrap(self.0.into_inner()).expect("unique custom arc") + } +} + +impl<'de> Deserialize<'de> for Custom { + fn deserialize>(de: D) -> Result { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = HashMap; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("a map") } + + fn visit_map>(self, mut map: M) -> Result { + let mut sections = HashMap::with_capacity(map.size_hint().unwrap_or(0)); + while let Some(key) = map.next_key::()? { + let section = map.next_value::()?; + 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 { + 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>(self, mut map: M) -> Result { + let mut sections = self.0.unwrap_unchecked(); + while let Some(key) = map.next_key::()? { + let (key, new) = (key.into_snake_cased(), map.next_value::()?); + 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)) + } +} diff --git a/yazi-config/src/theme/custom_field.rs b/yazi-config/src/theme/custom_field.rs new file mode 100644 index 00000000..c559f9ff --- /dev/null +++ b/yazi-config/src/theme/custom_field.rs @@ -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() } +} diff --git a/yazi-config/src/theme/custom_section.rs b/yazi-config/src/theme/custom_section.rs new file mode 100644 index 00000000..0335b69e --- /dev/null +++ b/yazi-config/src/theme/custom_section.rs @@ -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>); + +impl Deref for CustomSection { + type Target = ArcSwap>; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From> for CustomSection { + fn from(value: HashMap) -> Self { Self(value.into_pointee()) } +} + +impl CustomSection { + pub(super) fn unwrap_unchecked(self) -> HashMap { + Arc::try_unwrap(self.0.into_inner()).expect("unique custom section arc") + } +} + +impl<'de> Deserialize<'de> for CustomSection { + fn deserialize>(de: D) -> Result { + 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>(self, mut map: M) -> Result { + let mut fields = HashMap::with_capacity(map.size_hint().unwrap_or(0)); + while let Some(k) = map.next_key::()? { + fields.insert(k, map.next_value::()?); + } + Ok(CustomSection(fields.into_pointee())) + } + + fn visit_bool(self, _: bool) -> Result { + Ok(CustomSection::default()) + } + + fn visit_i64(self, _: i64) -> Result { + Ok(CustomSection::default()) + } + + fn visit_u64(self, _: u64) -> Result { + Ok(CustomSection::default()) + } + + fn visit_f64(self, _: f64) -> Result { + Ok(CustomSection::default()) + } + + fn visit_str(self, _: &str) -> Result { + Ok(CustomSection::default()) + } + + fn visit_bytes(self, _: &[u8]) -> Result { + Ok(CustomSection::default()) + } + + fn visit_none(self) -> Result { Ok(CustomSection::default()) } + + fn visit_unit(self) -> Result { Ok(CustomSection::default()) } + + fn visit_some>(self, de: D2) -> Result { + CustomSection::deserialize(de) + } + + fn visit_seq>(self, mut seq: A) -> Result { + while seq.next_element::()?.is_some() {} + Ok(CustomSection::default()) + } + } + + de.deserialize_any(V) + } +} diff --git a/yazi-config/src/theme/mod.rs b/yazi-config/src/theme/mod.rs index 25d45cec..4637adfd 100644 --- a/yazi-config/src/theme/mod.rs +++ b/yazi-config/src/theme/mod.rs @@ -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); diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index f8e19784..6f551911 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -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)] diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 68e741c6..0920aa6e 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -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" diff --git a/yazi-plugin/src/theme/theme.rs b/yazi-plugin/src/theme/theme.rs index 9a1bc266..554b2027 100644 --- a/yazi-plugin/src/theme/theme.rs +++ b/yazi-plugin/src/theme/theme.rs @@ -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 { 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 { Composer::new(get, set) } + +fn custom(lua: &Lua, key: &[u8]) -> mlua::Result { + match THEME.custom.load().get(str::from_utf8(key)?) { + Some(section) => CustomSection::new(section.load_full()).into_lua(lua), + None => Ok(Value::Nil), + } +} diff --git a/yazi-shared/src/bytes.rs b/yazi-shared/src/bytes.rs index 0492b947..4d0c56f0 100644 --- a/yazi-shared/src/bytes.rs +++ b/yazi-shared/src/bytes.rs @@ -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); diff --git a/yazi-shared/src/kebab_cased_string.rs b/yazi-shared/src/kebab_cased_string.rs new file mode 100644 index 00000000..01ef687b --- /dev/null +++ b/yazi-shared/src/kebab_cased_string.rs @@ -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 { 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 for KebabCasedString { + #[inline] + fn borrow(&self) -> &str { &self.0 } +} + +impl Borrow for KebabCasedString { + #[inline] + fn borrow(&self) -> &String { &self.0 } +} + +impl AsRef for KebabCasedString { + #[inline] + fn as_ref(&self) -> &str { &self.0 } +} + +impl AsRef 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 for String { + #[inline] + fn from(value: KebabCasedString) -> Self { value.0 } +} + +impl From for Cow<'_, str> { + #[inline] + fn from(value: KebabCasedString) -> Self { Cow::Owned(value.0) } +} + +impl<'de> Deserialize<'de> for KebabCasedString { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a kebab-cased string")) + } +} diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 666e423d..61039c03 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -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); diff --git a/yazi-shared/src/non_empty_string.rs b/yazi-shared/src/non_empty_string.rs index 1a6efbe7..8a801c97 100644 --- a/yazi-shared/src/non_empty_string.rs +++ b/yazi-shared/src/non_empty_string.rs @@ -57,6 +57,6 @@ impl From for OsString { impl<'de> Deserialize<'de> for NonEmptyString { fn deserialize>(deserializer: D) -> Result { 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")) } } diff --git a/yazi-shared/src/snake_cased_string.rs b/yazi-shared/src/snake_cased_string.rs new file mode 100644 index 00000000..bcdd6ed8 --- /dev/null +++ b/yazi-shared/src/snake_cased_string.rs @@ -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 { 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 for SnakeCasedString { + #[inline] + fn borrow(&self) -> &str { &self.0 } +} + +impl Borrow for SnakeCasedString { + #[inline] + fn borrow(&self) -> &String { &self.0 } +} + +impl AsRef for SnakeCasedString { + #[inline] + fn as_ref(&self) -> &str { &self.0 } +} + +impl AsRef 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 for String { + #[inline] + fn from(value: SnakeCasedString) -> Self { value.0 } +} + +impl<'de> Deserialize<'de> for SnakeCasedString { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a snake-cased string")) + } +} diff --git a/yazi-shim/src/serde/map.rs b/yazi-shim/src/serde/map.rs new file mode 100644 index 00000000..fab688c1 --- /dev/null +++ b/yazi-shim/src/serde/map.rs @@ -0,0 +1,37 @@ +use std::borrow::Cow; + +use serde::{Deserializer, de::{DeserializeSeed, IntoDeserializer, MapAccess, value::MapAccessDeserializer}}; + +struct SingleMapEntryAccess<'k, 'a, M> { + key: Option>, + 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>( + &mut self, + seed: K, + ) -> Result, Self::Error> { + match self.key.take() { + Some(k) => seed.deserialize(k.into_deserializer()).map(Some), + None => Ok(None), + } + } + + fn next_value_seed>(&mut self, seed: V) -> Result { + 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>, + M: MapAccess<'de>, +{ + MapAccessDeserializer::new(SingleMapEntryAccess { key: Some(key.into()), map }) +} diff --git a/yazi-shim/src/serde/mod.rs b/yazi-shim/src/serde/mod.rs index ff9b6f21..138dc3af 100644 --- a/yazi-shim/src/serde/mod.rs +++ b/yazi-shim/src/serde/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(traits); +yazi_macro::mod_flat!(map traits);