From 03f3f6df350b30599d3b9765100e810dbd21c12e Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Fri, 19 Sep 2025 14:57:55 -0400 Subject: [PATCH] feat(style): allow add/sub modifiers to be omitted in Style serialization. (#2057) It's really useful that Style supports Deserialize, this allows TUI apps to have configurable theming without much extra code. However, deserializing a style currently fails if `add_modifier` and `sub_modifier` are not specified. That means the following TOML config: ```toml [theme.highlight] fg = "white" bg = "black" ``` Will fail to deserialize with "missing field `add_modifier`". It should be possible to omit modifiers and have them default to "none". --- ratatui-core/src/style.rs | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ratatui-core/src/style.rs b/ratatui-core/src/style.rs index f238e441..44ec0d2f 100644 --- a/ratatui-core/src/style.rs +++ b/ratatui-core/src/style.rs @@ -238,15 +238,26 @@ impl fmt::Debug for Modifier { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Style { /// The foreground color. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub fg: Option, /// The background color. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub bg: Option, /// The underline color. #[cfg(feature = "underline-color")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub underline_color: Option, /// The modifiers to add. + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Modifier::is_empty") + )] pub add_modifier: Modifier, /// The modifiers to remove. + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Modifier::is_empty") + )] pub sub_modifier: Modifier, } @@ -923,4 +934,59 @@ mod tests { .remove_modifier(Modifier::DIM) ); } + + #[cfg(feature = "serde")] + #[test] + fn serialize_then_deserialize() { + let style = Style { + fg: Some(Color::Rgb(255, 0, 255)), + bg: Some(Color::White), + #[cfg(feature = "underline-color")] + underline_color: Some(Color::Indexed(3)), + add_modifier: Modifier::UNDERLINED, + sub_modifier: Modifier::CROSSED_OUT, + }; + + let json_str = serde_json::to_string(&style).unwrap(); + let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap(); + + let mut expected_json = serde_json::json!({ + "fg": "#FF00FF", + "bg": "White", + "add_modifier": "UNDERLINED", + "sub_modifier": "CROSSED_OUT" + }); + + #[cfg(feature = "underline-color")] + { + expected_json + .as_object_mut() + .unwrap() + .insert("underline_color".into(), "3".into()); + } + + assert_eq!(json_value, expected_json); + + let deserialized: Style = serde_json::from_str(&json_str).unwrap(); + assert_eq!(deserialized, style); + } + + #[cfg(feature = "serde")] + #[test] + fn deserialize_defaults() { + let style = Style { + fg: None, + bg: None, + #[cfg(feature = "underline-color")] + underline_color: None, + add_modifier: Modifier::empty(), + sub_modifier: Modifier::empty(), + }; + + let json_str = serde_json::to_string(&style).unwrap(); + assert_eq!(json_str, "{}"); + + let deserialized: Style = serde_json::from_str(&json_str).unwrap(); + assert_eq!(deserialized, style); + } }