cfgd_core/config/
theme.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize)]
4#[serde(rename_all = "camelCase")]
5pub struct ThemeConfig {
6 #[serde(default = "default_theme_name")]
7 pub name: String,
8 #[serde(default, skip_serializing_if = "ThemeOverrides::is_empty")]
9 pub overrides: ThemeOverrides,
10}
11
12fn default_theme_name() -> String {
13 "default".to_string()
14}
15
16impl Default for ThemeConfig {
17 fn default() -> Self {
18 Self {
19 name: default_theme_name(),
20 overrides: ThemeOverrides::default(),
21 }
22 }
23}
24
25impl<'de> serde::Deserialize<'de> for ThemeConfig {
27 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
28 where
29 D: serde::Deserializer<'de>,
30 {
31 use serde::de;
32
33 struct ThemeVisitor;
34 impl<'de> de::Visitor<'de> for ThemeVisitor {
35 type Value = ThemeConfig;
36 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
37 f.write_str("a theme name string or a theme config mapping")
38 }
39 fn visit_str<E: de::Error>(self, v: &str) -> std::result::Result<ThemeConfig, E> {
40 Ok(ThemeConfig {
41 name: v.to_string(),
42 overrides: ThemeOverrides::default(),
43 })
44 }
45 fn visit_map<M: de::MapAccess<'de>>(
46 self,
47 map: M,
48 ) -> std::result::Result<ThemeConfig, M::Error> {
49 #[derive(Deserialize)]
50 #[serde(rename_all = "camelCase")]
51 struct Inner {
52 #[serde(default = "default_theme_name")]
53 name: String,
54 #[serde(default)]
55 overrides: ThemeOverrides,
56 }
57 let inner = Inner::deserialize(de::value::MapAccessDeserializer::new(map))?;
58 Ok(ThemeConfig {
59 name: inner.name,
60 overrides: inner.overrides,
61 })
62 }
63 }
64 deserializer.deserialize_any(ThemeVisitor)
65 }
66}
67
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct ThemeOverrides {
75 pub header: Option<String>,
77 pub success: Option<String>,
78 pub warning: Option<String>,
79 pub error: Option<String>,
80 pub info: Option<String>,
81 pub muted: Option<String>,
82 pub running: Option<String>,
83 pub diff_add: Option<String>,
84 pub diff_remove: Option<String>,
85 pub diff_context: Option<String>,
86 pub accent: Option<String>,
87 pub secondary: Option<String>,
88
89 pub icon_ok: Option<String>,
91 pub icon_warn: Option<String>,
92 pub icon_fail: Option<String>,
93 pub icon_pending: Option<String>,
94 pub icon_running: Option<String>,
95 pub icon_skipped: Option<String>,
96 pub icon_arrow: Option<String>,
97}
98
99impl ThemeOverrides {
100 pub fn is_empty(&self) -> bool {
101 self.header.is_none()
102 && self.success.is_none()
103 && self.warning.is_none()
104 && self.error.is_none()
105 && self.info.is_none()
106 && self.muted.is_none()
107 && self.running.is_none()
108 && self.diff_add.is_none()
109 && self.diff_remove.is_none()
110 && self.diff_context.is_none()
111 && self.accent.is_none()
112 && self.secondary.is_none()
113 && self.icon_ok.is_none()
114 && self.icon_warn.is_none()
115 && self.icon_fail.is_none()
116 && self.icon_pending.is_none()
117 && self.icon_running.is_none()
118 && self.icon_skipped.is_none()
119 && self.icon_arrow.is_none()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn default_theme_config_uses_default_name() {
129 let tc = ThemeConfig::default();
130 assert_eq!(tc.name, "default");
131 assert!(tc.overrides.is_empty());
132 }
133
134 #[test]
135 fn deserialize_string_shorthand() {
136 let tc: ThemeConfig = serde_yaml::from_str("\"dracula\"").unwrap();
137 assert_eq!(tc.name, "dracula");
138 assert!(tc.overrides.is_empty());
139 }
140
141 #[test]
142 fn deserialize_map_with_name_only() {
143 let tc: ThemeConfig = serde_yaml::from_str("name: monokai").unwrap();
144 assert_eq!(tc.name, "monokai");
145 assert!(tc.overrides.is_empty());
146 }
147
148 #[test]
149 fn deserialize_map_with_overrides() {
150 let yaml = r##"
151name: custom
152overrides:
153 header: "#ff0000"
154 iconOk: "Y"
155"##;
156 let tc: ThemeConfig = serde_yaml::from_str(yaml).unwrap();
157 assert_eq!(tc.name, "custom");
158 assert_eq!(tc.overrides.header.as_deref(), Some("#ff0000"));
159 assert_eq!(tc.overrides.icon_ok.as_deref(), Some("Y"));
160 assert!(!tc.overrides.is_empty());
161 }
162
163 #[test]
164 fn deserialize_map_defaults_name_when_omitted() {
165 let tc: ThemeConfig = serde_yaml::from_str("overrides: {}").unwrap();
166 assert_eq!(tc.name, "default");
167 }
168
169 #[test]
170 fn overrides_is_empty_when_default() {
171 let o = ThemeOverrides::default();
172 assert!(o.is_empty());
173 }
174
175 #[test]
176 fn overrides_not_empty_when_any_field_set() {
177 let o = ThemeOverrides {
178 error: Some("#f00".to_string()),
179 ..ThemeOverrides::default()
180 };
181 assert!(!o.is_empty());
182 }
183}