1use std::collections::HashMap;
63use std::path::{Path, PathBuf};
64
65use console::Style;
66
67use super::super::style::{
68 parse_stylesheet, StyleValidationError, StyleValue, Styles, StylesheetError, ThemeVariants,
69};
70
71use super::adaptive::ColorMode;
72use super::icon_def::{IconDefinition, IconSet};
73use super::icon_mode::IconMode;
74
75#[derive(Debug, Clone)]
115pub struct Theme {
116 name: Option<String>,
118 source_path: Option<PathBuf>,
120 base: HashMap<String, Style>,
122 light: HashMap<String, Style>,
124 dark: HashMap<String, Style>,
126 aliases: HashMap<String, String>,
128 icons: IconSet,
130}
131
132impl Theme {
133 pub fn new() -> Self {
135 Self {
136 name: None,
137 source_path: None,
138 base: HashMap::new(),
139 light: HashMap::new(),
140 dark: HashMap::new(),
141 aliases: HashMap::new(),
142 icons: IconSet::new(),
143 }
144 }
145
146 pub fn named(name: impl Into<String>) -> Self {
148 Self {
149 name: Some(name.into()),
150 source_path: None,
151 base: HashMap::new(),
152 light: HashMap::new(),
153 dark: HashMap::new(),
154 aliases: HashMap::new(),
155 icons: IconSet::new(),
156 }
157 }
158
159 pub fn with_name(mut self, name: impl Into<String>) -> Self {
164 self.name = Some(name.into());
165 self
166 }
167
168 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, StylesheetError> {
186 let path = path.as_ref();
187 let content = std::fs::read_to_string(path).map_err(|e| StylesheetError::Load {
188 message: format!("Failed to read {}: {}", path.display(), e),
189 })?;
190
191 let name = path
192 .file_stem()
193 .and_then(|s| s.to_str())
194 .map(|s| s.to_string());
195
196 let icons = parse_icons_from_yaml_str(&content)?;
197 let variants = parse_stylesheet(&content)?;
198 Ok(Self {
199 name,
200 source_path: Some(path.to_path_buf()),
201 base: variants.base().clone(),
202 light: variants.light().clone(),
203 dark: variants.dark().clone(),
204 aliases: variants.aliases().clone(),
205 icons,
206 })
207 }
208
209 pub fn from_yaml(yaml: &str) -> Result<Self, StylesheetError> {
240 let icons = parse_icons_from_yaml_str(yaml)?;
241 let variants = parse_stylesheet(yaml)?;
242 Ok(Self {
243 name: None,
244 source_path: None,
245 base: variants.base().clone(),
246 light: variants.light().clone(),
247 dark: variants.dark().clone(),
248 aliases: variants.aliases().clone(),
249 icons,
250 })
251 }
252
253 pub fn from_variants(variants: ThemeVariants) -> Self {
255 Self {
256 name: None,
257 source_path: None,
258 base: variants.base().clone(),
259 light: variants.light().clone(),
260 dark: variants.dark().clone(),
261 aliases: variants.aliases().clone(),
262 icons: IconSet::new(),
263 }
264 }
265
266 pub fn name(&self) -> Option<&str> {
271 self.name.as_deref()
272 }
273
274 pub fn source_path(&self) -> Option<&Path> {
276 self.source_path.as_deref()
277 }
278
279 pub fn refresh(&mut self) -> Result<(), StylesheetError> {
299 let path = self
300 .source_path
301 .as_ref()
302 .ok_or_else(|| StylesheetError::Load {
303 message: "Cannot refresh: theme has no source file".to_string(),
304 })?;
305
306 let content = std::fs::read_to_string(path).map_err(|e| StylesheetError::Load {
307 message: format!("Failed to read {}: {}", path.display(), e),
308 })?;
309
310 let icons = parse_icons_from_yaml_str(&content)?;
311 let variants = parse_stylesheet(&content)?;
312 self.base = variants.base().clone();
313 self.light = variants.light().clone();
314 self.dark = variants.dark().clone();
315 self.aliases = variants.aliases().clone();
316 self.icons = icons;
317
318 Ok(())
319 }
320
321 pub fn add<V: Into<StyleValue>>(mut self, name: &str, value: V) -> Self {
348 match value.into() {
349 StyleValue::Concrete(style) => {
350 self.base.insert(name.to_string(), style);
351 }
352 StyleValue::Alias(target) => {
353 self.aliases.insert(name.to_string(), target);
354 }
355 }
356 self
357 }
358
359 pub fn add_adaptive(
380 mut self,
381 name: &str,
382 base: Style,
383 light: Option<Style>,
384 dark: Option<Style>,
385 ) -> Self {
386 self.base.insert(name.to_string(), base);
387 if let Some(light_style) = light {
388 self.light.insert(name.to_string(), light_style);
389 }
390 if let Some(dark_style) = dark {
391 self.dark.insert(name.to_string(), dark_style);
392 }
393 self
394 }
395
396 pub fn add_icon(mut self, name: &str, def: IconDefinition) -> Self {
412 self.icons.insert(name.to_string(), def);
413 self
414 }
415
416 pub fn resolve_icons(&self, mode: IconMode) -> HashMap<String, String> {
435 self.icons.resolve(mode)
436 }
437
438 pub fn icons(&self) -> &IconSet {
440 &self.icons
441 }
442
443 pub fn resolve_styles(&self, mode: Option<ColorMode>) -> Styles {
471 let mut styles = Styles::new();
472
473 let mode_overrides = match mode {
475 Some(ColorMode::Light) => &self.light,
476 Some(ColorMode::Dark) => &self.dark,
477 None => &HashMap::new(),
478 };
479
480 for (name, base_style) in &self.base {
482 let style = mode_overrides.get(name).unwrap_or(base_style);
483 styles = styles.add(name, style.clone());
484 }
485
486 for (name, target) in &self.aliases {
488 styles = styles.add(name, target.clone());
489 }
490
491 styles
492 }
493
494 pub fn validate(&self) -> Result<(), StyleValidationError> {
499 self.resolve_styles(None).validate()
501 }
502
503 pub fn is_empty(&self) -> bool {
505 self.base.is_empty() && self.aliases.is_empty()
506 }
507
508 pub fn len(&self) -> usize {
510 self.base.len() + self.aliases.len()
511 }
512
513 pub fn get_style(&self, name: &str, mode: Option<ColorMode>) -> Option<Style> {
517 let styles = self.resolve_styles(mode);
518 styles.resolve(name).cloned()
524 }
525
526 pub fn light_override_count(&self) -> usize {
528 self.light.len()
529 }
530
531 pub fn dark_override_count(&self) -> usize {
533 self.dark.len()
534 }
535
536 pub fn merge(mut self, other: Theme) -> Self {
554 self.base.extend(other.base);
555 self.light.extend(other.light);
556 self.dark.extend(other.dark);
557 self.aliases.extend(other.aliases);
558 self.icons = self.icons.merge(other.icons);
559 self
560 }
561}
562
563impl Default for Theme {
564 fn default() -> Self {
565 Self::new()
566 }
567}
568
569fn parse_icons_from_yaml_str(yaml: &str) -> Result<IconSet, StylesheetError> {
587 let root: serde_yaml::Value =
588 serde_yaml::from_str(yaml).map_err(|e| StylesheetError::Parse {
589 path: None,
590 message: e.to_string(),
591 })?;
592
593 parse_icons_from_yaml(&root)
594}
595
596fn parse_icons_from_yaml(root: &serde_yaml::Value) -> Result<IconSet, StylesheetError> {
598 let mut icon_set = IconSet::new();
599
600 let mapping = match root.as_mapping() {
601 Some(m) => m,
602 None => return Ok(icon_set),
603 };
604
605 let icons_value = match mapping.get(serde_yaml::Value::String("icons".into())) {
606 Some(v) => v,
607 None => return Ok(icon_set),
608 };
609
610 let icons_map = icons_value
611 .as_mapping()
612 .ok_or_else(|| StylesheetError::Parse {
613 path: None,
614 message: "'icons' must be a mapping".to_string(),
615 })?;
616
617 for (key, value) in icons_map {
618 let name = key.as_str().ok_or_else(|| StylesheetError::Parse {
619 path: None,
620 message: format!("Icon name must be a string, got {:?}", key),
621 })?;
622
623 let def = match value {
624 serde_yaml::Value::String(s) => {
625 IconDefinition::new(s.clone())
627 }
628 serde_yaml::Value::Mapping(map) => {
629 let classic = map
630 .get(serde_yaml::Value::String("classic".into()))
631 .and_then(|v| v.as_str())
632 .ok_or_else(|| StylesheetError::InvalidDefinition {
633 style: name.to_string(),
634 message: "Icon mapping must have a 'classic' key".to_string(),
635 path: None,
636 })?;
637 let nerdfont = map
638 .get(serde_yaml::Value::String("nerdfont".into()))
639 .and_then(|v| v.as_str());
640 let mut def = IconDefinition::new(classic);
641 if let Some(nf) = nerdfont {
642 def = def.with_nerdfont(nf);
643 }
644 def
645 }
646 _ => {
647 return Err(StylesheetError::InvalidDefinition {
648 style: name.to_string(),
649 message: "Icon must be a string or mapping with 'classic' key".to_string(),
650 path: None,
651 });
652 }
653 };
654
655 icon_set.insert(name.to_string(), def);
656 }
657
658 Ok(icon_set)
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 fn test_theme_new_is_empty() {
667 let theme = Theme::new();
668 assert!(theme.is_empty());
669 assert_eq!(theme.len(), 0);
670 }
671
672 #[test]
673 fn test_theme_add_concrete() {
674 let theme = Theme::new().add("bold", Style::new().bold());
675 assert!(!theme.is_empty());
676 assert_eq!(theme.len(), 1);
677 }
678
679 #[test]
680 fn test_theme_add_alias_str() {
681 let theme = Theme::new()
682 .add("base", Style::new().dim())
683 .add("alias", "base");
684
685 assert_eq!(theme.len(), 2);
686
687 let styles = theme.resolve_styles(None);
688 assert!(styles.has("base"));
689 assert!(styles.has("alias"));
690 }
691
692 #[test]
693 fn test_theme_add_alias_string() {
694 let target = String::from("base");
695 let theme = Theme::new()
696 .add("base", Style::new().dim())
697 .add("alias", target);
698
699 let styles = theme.resolve_styles(None);
700 assert!(styles.has("alias"));
701 }
702
703 #[test]
704 fn test_theme_validate_valid() {
705 let theme = Theme::new()
706 .add("visual", Style::new().cyan())
707 .add("semantic", "visual");
708
709 assert!(theme.validate().is_ok());
710 }
711
712 #[test]
713 fn test_theme_validate_invalid() {
714 let theme = Theme::new().add("orphan", "missing");
715 assert!(theme.validate().is_err());
716 }
717
718 #[test]
719 fn test_theme_default() {
720 let theme = Theme::default();
721 assert!(theme.is_empty());
722 }
723
724 #[test]
729 fn test_theme_add_adaptive() {
730 let theme = Theme::new().add_adaptive(
731 "panel",
732 Style::new().dim(),
733 Some(Style::new().bold()),
734 Some(Style::new().italic()),
735 );
736
737 assert_eq!(theme.len(), 1);
738 assert_eq!(theme.light_override_count(), 1);
739 assert_eq!(theme.dark_override_count(), 1);
740 }
741
742 #[test]
743 fn test_theme_add_adaptive_light_only() {
744 let theme =
745 Theme::new().add_adaptive("panel", Style::new().dim(), Some(Style::new().bold()), None);
746
747 assert_eq!(theme.light_override_count(), 1);
748 assert_eq!(theme.dark_override_count(), 0);
749 }
750
751 #[test]
752 fn test_theme_add_adaptive_dark_only() {
753 let theme =
754 Theme::new().add_adaptive("panel", Style::new().dim(), None, Some(Style::new().bold()));
755
756 assert_eq!(theme.light_override_count(), 0);
757 assert_eq!(theme.dark_override_count(), 1);
758 }
759
760 #[test]
761 fn test_theme_resolve_styles_no_mode() {
762 let theme = Theme::new()
763 .add("header", Style::new().cyan())
764 .add_adaptive(
765 "panel",
766 Style::new().dim(),
767 Some(Style::new().bold()),
768 Some(Style::new().italic()),
769 );
770
771 let styles = theme.resolve_styles(None);
772 assert!(styles.has("header"));
773 assert!(styles.has("panel"));
774 }
775
776 #[test]
777 fn test_theme_resolve_styles_light_mode() {
778 let theme = Theme::new().add_adaptive(
779 "panel",
780 Style::new().dim(),
781 Some(Style::new().bold()),
782 Some(Style::new().italic()),
783 );
784
785 let styles = theme.resolve_styles(Some(ColorMode::Light));
786 assert!(styles.has("panel"));
787 }
790
791 #[test]
792 fn test_theme_resolve_styles_dark_mode() {
793 let theme = Theme::new().add_adaptive(
794 "panel",
795 Style::new().dim(),
796 Some(Style::new().bold()),
797 Some(Style::new().italic()),
798 );
799
800 let styles = theme.resolve_styles(Some(ColorMode::Dark));
801 assert!(styles.has("panel"));
802 }
803
804 #[test]
805 fn test_theme_resolve_styles_preserves_aliases() {
806 let theme = Theme::new()
807 .add("base", Style::new().dim())
808 .add("alias", "base");
809
810 let styles = theme.resolve_styles(Some(ColorMode::Light));
811 assert!(styles.has("base"));
812 assert!(styles.has("alias"));
813
814 assert!(styles.validate().is_ok());
816 }
817
818 #[test]
823 fn test_theme_from_yaml_simple() {
824 let theme = Theme::from_yaml(
825 r#"
826 header:
827 fg: cyan
828 bold: true
829 "#,
830 )
831 .unwrap();
832
833 assert_eq!(theme.len(), 1);
834 let styles = theme.resolve_styles(None);
835 assert!(styles.has("header"));
836 }
837
838 #[test]
839 fn test_theme_from_yaml_shorthand() {
840 let theme = Theme::from_yaml(
841 r#"
842 bold_text: bold
843 accent: cyan
844 warning: "yellow italic"
845 "#,
846 )
847 .unwrap();
848
849 assert_eq!(theme.len(), 3);
850 }
851
852 #[test]
853 fn test_theme_from_yaml_alias() {
854 let theme = Theme::from_yaml(
855 r#"
856 muted:
857 dim: true
858 disabled: muted
859 "#,
860 )
861 .unwrap();
862
863 assert_eq!(theme.len(), 2);
864 assert!(theme.validate().is_ok());
865 }
866
867 #[test]
868 fn test_theme_from_yaml_adaptive() {
869 let theme = Theme::from_yaml(
870 r#"
871 panel:
872 fg: gray
873 light:
874 fg: black
875 dark:
876 fg: white
877 "#,
878 )
879 .unwrap();
880
881 assert_eq!(theme.len(), 1);
882 assert_eq!(theme.light_override_count(), 1);
883 assert_eq!(theme.dark_override_count(), 1);
884 }
885
886 #[test]
887 fn test_theme_from_yaml_invalid() {
888 let result = Theme::from_yaml("not valid yaml: [");
889 assert!(result.is_err());
890 }
891
892 #[test]
893 fn test_theme_from_yaml_complete() {
894 let theme = Theme::from_yaml(
895 r##"
896 # Visual layer
897 muted:
898 dim: true
899
900 accent:
901 fg: cyan
902 bold: true
903
904 # Adaptive
905 background:
906 light:
907 bg: "#f8f8f8"
908 dark:
909 bg: "#1e1e1e"
910
911 # Aliases
912 header: accent
913 footer: muted
914 "##,
915 )
916 .unwrap();
917
918 assert_eq!(theme.len(), 5);
920 assert!(theme.validate().is_ok());
921
922 assert_eq!(theme.light_override_count(), 1);
924 assert_eq!(theme.dark_override_count(), 1);
925 }
926
927 #[test]
932 fn test_theme_named() {
933 let theme = Theme::named("darcula");
934 assert_eq!(theme.name(), Some("darcula"));
935 assert!(theme.is_empty());
936 }
937
938 #[test]
939 fn test_theme_new_has_no_name() {
940 let theme = Theme::new();
941 assert_eq!(theme.name(), None);
942 assert_eq!(theme.source_path(), None);
943 }
944
945 #[test]
946 fn test_theme_from_file() {
947 use std::fs;
948 use tempfile::TempDir;
949
950 let temp_dir = TempDir::new().unwrap();
951 let theme_path = temp_dir.path().join("darcula.yaml");
952 fs::write(
953 &theme_path,
954 r#"
955 header:
956 fg: cyan
957 bold: true
958 muted:
959 dim: true
960 "#,
961 )
962 .unwrap();
963
964 let theme = Theme::from_file(&theme_path).unwrap();
965 assert_eq!(theme.name(), Some("darcula"));
966 assert_eq!(theme.source_path(), Some(theme_path.as_path()));
967 assert_eq!(theme.len(), 2);
968 }
969
970 #[test]
971 fn test_theme_from_file_not_found() {
972 let result = Theme::from_file("/nonexistent/path/theme.yaml");
973 assert!(result.is_err());
974 }
975
976 #[test]
977 fn test_theme_refresh() {
978 use std::fs;
979 use tempfile::TempDir;
980
981 let temp_dir = TempDir::new().unwrap();
982 let theme_path = temp_dir.path().join("dynamic.yaml");
983 fs::write(
984 &theme_path,
985 r#"
986 header:
987 fg: red
988 "#,
989 )
990 .unwrap();
991
992 let mut theme = Theme::from_file(&theme_path).unwrap();
993 assert_eq!(theme.len(), 1);
994
995 fs::write(
997 &theme_path,
998 r#"
999 header:
1000 fg: blue
1001 footer:
1002 dim: true
1003 "#,
1004 )
1005 .unwrap();
1006
1007 theme.refresh().unwrap();
1009 assert_eq!(theme.len(), 2);
1010 }
1011
1012 #[test]
1013 fn test_theme_refresh_without_source() {
1014 let mut theme = Theme::new();
1015 let result = theme.refresh();
1016 assert!(result.is_err());
1017 }
1018
1019 #[test]
1020 fn test_theme_merge() {
1021 let base = Theme::new()
1022 .add("keep", Style::new().dim())
1023 .add("overwrite", Style::new().red());
1024
1025 let extension = Theme::new()
1026 .add("overwrite", Style::new().blue())
1027 .add("new", Style::new().bold());
1028
1029 let merged = base.merge(extension);
1030
1031 let styles = merged.resolve_styles(None);
1032
1033 assert!(styles.has("keep"));
1035
1036 assert!(styles.has("overwrite"));
1038
1039 assert!(styles.has("new"));
1041
1042 assert_eq!(merged.len(), 3);
1043 }
1044
1045 #[test]
1050 fn test_theme_add_icon() {
1051 let theme = Theme::new()
1052 .add_icon("pending", IconDefinition::new("⚪"))
1053 .add_icon("done", IconDefinition::new("⚫").with_nerdfont("\u{f00c}"));
1054
1055 assert_eq!(theme.icons().len(), 2);
1056 assert!(!theme.icons().is_empty());
1057 }
1058
1059 #[test]
1060 fn test_theme_resolve_icons_classic() {
1061 let theme = Theme::new()
1062 .add_icon("pending", IconDefinition::new("⚪"))
1063 .add_icon("done", IconDefinition::new("⚫").with_nerdfont("\u{f00c}"));
1064
1065 let resolved = theme.resolve_icons(IconMode::Classic);
1066 assert_eq!(resolved.get("pending").unwrap(), "⚪");
1067 assert_eq!(resolved.get("done").unwrap(), "⚫");
1068 }
1069
1070 #[test]
1071 fn test_theme_resolve_icons_nerdfont() {
1072 let theme = Theme::new()
1073 .add_icon("pending", IconDefinition::new("⚪"))
1074 .add_icon("done", IconDefinition::new("⚫").with_nerdfont("\u{f00c}"));
1075
1076 let resolved = theme.resolve_icons(IconMode::NerdFont);
1077 assert_eq!(resolved.get("pending").unwrap(), "⚪"); assert_eq!(resolved.get("done").unwrap(), "\u{f00c}");
1079 }
1080
1081 #[test]
1082 fn test_theme_icons_empty_by_default() {
1083 let theme = Theme::new();
1084 assert!(theme.icons().is_empty());
1085 }
1086
1087 #[test]
1088 fn test_theme_merge_with_icons() {
1089 let base = Theme::new()
1090 .add_icon("keep", IconDefinition::new("K"))
1091 .add_icon("override", IconDefinition::new("OLD"));
1092
1093 let ext = Theme::new()
1094 .add_icon("override", IconDefinition::new("NEW"))
1095 .add_icon("added", IconDefinition::new("A"));
1096
1097 let merged = base.merge(ext);
1098 assert_eq!(merged.icons().len(), 3);
1099
1100 let resolved = merged.resolve_icons(IconMode::Classic);
1101 assert_eq!(resolved.get("keep").unwrap(), "K");
1102 assert_eq!(resolved.get("override").unwrap(), "NEW");
1103 assert_eq!(resolved.get("added").unwrap(), "A");
1104 }
1105
1106 #[test]
1107 fn test_theme_from_yaml_with_icons() {
1108 let theme = Theme::from_yaml(
1109 r#"
1110 header:
1111 fg: cyan
1112 bold: true
1113 icons:
1114 pending: "⚪"
1115 done:
1116 classic: "⚫"
1117 nerdfont: "nf_done"
1118 "#,
1119 )
1120 .unwrap();
1121
1122 assert_eq!(theme.len(), 1);
1124 let styles = theme.resolve_styles(None);
1125 assert!(styles.has("header"));
1126
1127 assert_eq!(theme.icons().len(), 2);
1129 let resolved = theme.resolve_icons(IconMode::Classic);
1130 assert_eq!(resolved.get("pending").unwrap(), "⚪");
1131 assert_eq!(resolved.get("done").unwrap(), "⚫");
1132
1133 let resolved = theme.resolve_icons(IconMode::NerdFont);
1134 assert_eq!(resolved.get("done").unwrap(), "nf_done");
1135 }
1136
1137 #[test]
1138 fn test_theme_from_yaml_no_icons() {
1139 let theme = Theme::from_yaml(
1140 r#"
1141 header:
1142 fg: cyan
1143 "#,
1144 )
1145 .unwrap();
1146
1147 assert!(theme.icons().is_empty());
1148 }
1149
1150 #[test]
1151 fn test_theme_from_yaml_icons_only() {
1152 let theme = Theme::from_yaml(
1153 r#"
1154 icons:
1155 check: "✓"
1156 "#,
1157 )
1158 .unwrap();
1159
1160 assert_eq!(theme.icons().len(), 1);
1161 assert_eq!(theme.len(), 0); }
1163
1164 #[test]
1165 fn test_theme_from_yaml_icons_invalid_type() {
1166 let result = Theme::from_yaml(
1167 r#"
1168 icons:
1169 bad: 42
1170 "#,
1171 );
1172 assert!(result.is_err());
1173 }
1174
1175 #[test]
1176 fn test_theme_from_yaml_icons_mapping_without_classic() {
1177 let result = Theme::from_yaml(
1178 r#"
1179 icons:
1180 bad:
1181 nerdfont: "nf"
1182 "#,
1183 );
1184 assert!(result.is_err());
1185 }
1186
1187 #[test]
1188 fn test_theme_from_file_with_icons() {
1189 use std::fs;
1190 use tempfile::TempDir;
1191
1192 let temp_dir = TempDir::new().unwrap();
1193 let theme_path = temp_dir.path().join("iconic.yaml");
1194 fs::write(
1195 &theme_path,
1196 r#"
1197 header:
1198 fg: cyan
1199 icons:
1200 check:
1201 classic: "[ok]"
1202 nerdfont: "nf_check"
1203 "#,
1204 )
1205 .unwrap();
1206
1207 let theme = Theme::from_file(&theme_path).unwrap();
1208 assert_eq!(theme.icons().len(), 1);
1209 let resolved = theme.resolve_icons(IconMode::NerdFont);
1210 assert_eq!(resolved.get("check").unwrap(), "nf_check");
1211 }
1212
1213 #[test]
1214 fn test_theme_refresh_with_icons() {
1215 use std::fs;
1216 use tempfile::TempDir;
1217
1218 let temp_dir = TempDir::new().unwrap();
1219 let theme_path = temp_dir.path().join("refresh.yaml");
1220 fs::write(
1221 &theme_path,
1222 r#"
1223 icons:
1224 v1: "one"
1225 "#,
1226 )
1227 .unwrap();
1228
1229 let mut theme = Theme::from_file(&theme_path).unwrap();
1230 assert_eq!(theme.icons().len(), 1);
1231
1232 fs::write(
1233 &theme_path,
1234 r#"
1235 icons:
1236 v1: "one"
1237 v2: "two"
1238 "#,
1239 )
1240 .unwrap();
1241
1242 theme.refresh().unwrap();
1243 assert_eq!(theme.icons().len(), 2);
1244 }
1245}