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_css(css: &str) -> Result<Self, StylesheetError> {
274 let variants = crate::parse_css(css)?;
275 Ok(Self {
276 name: None,
277 source_path: None,
278 base: variants.base().clone(),
279 light: variants.light().clone(),
280 dark: variants.dark().clone(),
281 aliases: variants.aliases().clone(),
282 icons: IconSet::new(),
283 })
284 }
285
286 pub fn from_css_file<P: AsRef<Path>>(path: P) -> Result<Self, StylesheetError> {
304 let path = path.as_ref();
305 let content = std::fs::read_to_string(path).map_err(|e| StylesheetError::Load {
306 message: format!("Failed to read {}: {}", path.display(), e),
307 })?;
308
309 let name = path
310 .file_stem()
311 .and_then(|s| s.to_str())
312 .map(|s| s.to_string());
313
314 let variants = crate::parse_css(&content)?;
315 Ok(Self {
316 name,
317 source_path: Some(path.to_path_buf()),
318 base: variants.base().clone(),
319 light: variants.light().clone(),
320 dark: variants.dark().clone(),
321 aliases: variants.aliases().clone(),
322 icons: IconSet::new(),
323 })
324 }
325
326 pub fn from_variants(variants: ThemeVariants) -> Self {
328 Self {
329 name: None,
330 source_path: None,
331 base: variants.base().clone(),
332 light: variants.light().clone(),
333 dark: variants.dark().clone(),
334 aliases: variants.aliases().clone(),
335 icons: IconSet::new(),
336 }
337 }
338
339 pub fn name(&self) -> Option<&str> {
344 self.name.as_deref()
345 }
346
347 pub fn source_path(&self) -> Option<&Path> {
349 self.source_path.as_deref()
350 }
351
352 pub fn refresh(&mut self) -> Result<(), StylesheetError> {
372 let path = self
373 .source_path
374 .as_ref()
375 .ok_or_else(|| StylesheetError::Load {
376 message: "Cannot refresh: theme has no source file".to_string(),
377 })?;
378
379 let content = std::fs::read_to_string(path).map_err(|e| StylesheetError::Load {
380 message: format!("Failed to read {}: {}", path.display(), e),
381 })?;
382
383 let icons = parse_icons_from_yaml_str(&content)?;
384 let variants = parse_stylesheet(&content)?;
385 self.base = variants.base().clone();
386 self.light = variants.light().clone();
387 self.dark = variants.dark().clone();
388 self.aliases = variants.aliases().clone();
389 self.icons = icons;
390
391 Ok(())
392 }
393
394 pub fn add<V: Into<StyleValue>>(mut self, name: &str, value: V) -> Self {
421 match value.into() {
422 StyleValue::Concrete(style) => {
423 self.base.insert(name.to_string(), style);
424 }
425 StyleValue::Alias(target) => {
426 self.aliases.insert(name.to_string(), target);
427 }
428 }
429 self
430 }
431
432 pub fn add_adaptive(
453 mut self,
454 name: &str,
455 base: Style,
456 light: Option<Style>,
457 dark: Option<Style>,
458 ) -> Self {
459 self.base.insert(name.to_string(), base);
460 if let Some(light_style) = light {
461 self.light.insert(name.to_string(), light_style);
462 }
463 if let Some(dark_style) = dark {
464 self.dark.insert(name.to_string(), dark_style);
465 }
466 self
467 }
468
469 pub fn add_icon(mut self, name: &str, def: IconDefinition) -> Self {
485 self.icons.insert(name.to_string(), def);
486 self
487 }
488
489 pub fn resolve_icons(&self, mode: IconMode) -> HashMap<String, String> {
508 self.icons.resolve(mode)
509 }
510
511 pub fn icons(&self) -> &IconSet {
513 &self.icons
514 }
515
516 pub fn resolve_styles(&self, mode: Option<ColorMode>) -> Styles {
544 let mut styles = Styles::new();
545
546 let mode_overrides = match mode {
548 Some(ColorMode::Light) => &self.light,
549 Some(ColorMode::Dark) => &self.dark,
550 None => &HashMap::new(),
551 };
552
553 for (name, base_style) in &self.base {
555 let style = mode_overrides.get(name).unwrap_or(base_style);
556 styles = styles.add(name, style.clone());
557 }
558
559 for (name, target) in &self.aliases {
561 styles = styles.add(name, target.clone());
562 }
563
564 styles
565 }
566
567 pub fn validate(&self) -> Result<(), StyleValidationError> {
572 self.resolve_styles(None).validate()
574 }
575
576 pub fn is_empty(&self) -> bool {
578 self.base.is_empty() && self.aliases.is_empty()
579 }
580
581 pub fn len(&self) -> usize {
583 self.base.len() + self.aliases.len()
584 }
585
586 pub fn get_style(&self, name: &str, mode: Option<ColorMode>) -> Option<Style> {
590 let styles = self.resolve_styles(mode);
591 styles.resolve(name).cloned()
597 }
598
599 pub fn light_override_count(&self) -> usize {
601 self.light.len()
602 }
603
604 pub fn dark_override_count(&self) -> usize {
606 self.dark.len()
607 }
608
609 pub fn merge(mut self, other: Theme) -> Self {
627 self.base.extend(other.base);
628 self.light.extend(other.light);
629 self.dark.extend(other.dark);
630 self.aliases.extend(other.aliases);
631 self.icons = self.icons.merge(other.icons);
632 self
633 }
634}
635
636impl Default for Theme {
637 fn default() -> Self {
638 Self::new()
639 }
640}
641
642fn parse_icons_from_yaml_str(yaml: &str) -> Result<IconSet, StylesheetError> {
660 let root: serde_yaml::Value =
661 serde_yaml::from_str(yaml).map_err(|e| StylesheetError::Parse {
662 path: None,
663 message: e.to_string(),
664 })?;
665
666 parse_icons_from_yaml(&root)
667}
668
669fn parse_icons_from_yaml(root: &serde_yaml::Value) -> Result<IconSet, StylesheetError> {
671 let mut icon_set = IconSet::new();
672
673 let mapping = match root.as_mapping() {
674 Some(m) => m,
675 None => return Ok(icon_set),
676 };
677
678 let icons_value = match mapping.get(serde_yaml::Value::String("icons".into())) {
679 Some(v) => v,
680 None => return Ok(icon_set),
681 };
682
683 let icons_map = icons_value
684 .as_mapping()
685 .ok_or_else(|| StylesheetError::Parse {
686 path: None,
687 message: "'icons' must be a mapping".to_string(),
688 })?;
689
690 for (key, value) in icons_map {
691 let name = key.as_str().ok_or_else(|| StylesheetError::Parse {
692 path: None,
693 message: format!("Icon name must be a string, got {:?}", key),
694 })?;
695
696 let def = match value {
697 serde_yaml::Value::String(s) => {
698 IconDefinition::new(s.clone())
700 }
701 serde_yaml::Value::Mapping(map) => {
702 let classic = map
703 .get(serde_yaml::Value::String("classic".into()))
704 .and_then(|v| v.as_str())
705 .ok_or_else(|| StylesheetError::InvalidDefinition {
706 style: name.to_string(),
707 message: "Icon mapping must have a 'classic' key".to_string(),
708 path: None,
709 })?;
710 let nerdfont = map
711 .get(serde_yaml::Value::String("nerdfont".into()))
712 .and_then(|v| v.as_str());
713 let mut def = IconDefinition::new(classic);
714 if let Some(nf) = nerdfont {
715 def = def.with_nerdfont(nf);
716 }
717 def
718 }
719 _ => {
720 return Err(StylesheetError::InvalidDefinition {
721 style: name.to_string(),
722 message: "Icon must be a string or mapping with 'classic' key".to_string(),
723 path: None,
724 });
725 }
726 };
727
728 icon_set.insert(name.to_string(), def);
729 }
730
731 Ok(icon_set)
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn test_theme_new_is_empty() {
740 let theme = Theme::new();
741 assert!(theme.is_empty());
742 assert_eq!(theme.len(), 0);
743 }
744
745 #[test]
746 fn test_theme_add_concrete() {
747 let theme = Theme::new().add("bold", Style::new().bold());
748 assert!(!theme.is_empty());
749 assert_eq!(theme.len(), 1);
750 }
751
752 #[test]
753 fn test_theme_add_alias_str() {
754 let theme = Theme::new()
755 .add("base", Style::new().dim())
756 .add("alias", "base");
757
758 assert_eq!(theme.len(), 2);
759
760 let styles = theme.resolve_styles(None);
761 assert!(styles.has("base"));
762 assert!(styles.has("alias"));
763 }
764
765 #[test]
766 fn test_theme_add_alias_string() {
767 let target = String::from("base");
768 let theme = Theme::new()
769 .add("base", Style::new().dim())
770 .add("alias", target);
771
772 let styles = theme.resolve_styles(None);
773 assert!(styles.has("alias"));
774 }
775
776 #[test]
777 fn test_theme_validate_valid() {
778 let theme = Theme::new()
779 .add("visual", Style::new().cyan())
780 .add("semantic", "visual");
781
782 assert!(theme.validate().is_ok());
783 }
784
785 #[test]
786 fn test_theme_validate_invalid() {
787 let theme = Theme::new().add("orphan", "missing");
788 assert!(theme.validate().is_err());
789 }
790
791 #[test]
792 fn test_theme_default() {
793 let theme = Theme::default();
794 assert!(theme.is_empty());
795 }
796
797 #[test]
802 fn test_theme_add_adaptive() {
803 let theme = Theme::new().add_adaptive(
804 "panel",
805 Style::new().dim(),
806 Some(Style::new().bold()),
807 Some(Style::new().italic()),
808 );
809
810 assert_eq!(theme.len(), 1);
811 assert_eq!(theme.light_override_count(), 1);
812 assert_eq!(theme.dark_override_count(), 1);
813 }
814
815 #[test]
816 fn test_theme_add_adaptive_light_only() {
817 let theme =
818 Theme::new().add_adaptive("panel", Style::new().dim(), Some(Style::new().bold()), None);
819
820 assert_eq!(theme.light_override_count(), 1);
821 assert_eq!(theme.dark_override_count(), 0);
822 }
823
824 #[test]
825 fn test_theme_add_adaptive_dark_only() {
826 let theme =
827 Theme::new().add_adaptive("panel", Style::new().dim(), None, Some(Style::new().bold()));
828
829 assert_eq!(theme.light_override_count(), 0);
830 assert_eq!(theme.dark_override_count(), 1);
831 }
832
833 #[test]
834 fn test_theme_resolve_styles_no_mode() {
835 let theme = Theme::new()
836 .add("header", Style::new().cyan())
837 .add_adaptive(
838 "panel",
839 Style::new().dim(),
840 Some(Style::new().bold()),
841 Some(Style::new().italic()),
842 );
843
844 let styles = theme.resolve_styles(None);
845 assert!(styles.has("header"));
846 assert!(styles.has("panel"));
847 }
848
849 #[test]
850 fn test_theme_resolve_styles_light_mode() {
851 let theme = Theme::new().add_adaptive(
852 "panel",
853 Style::new().dim(),
854 Some(Style::new().bold()),
855 Some(Style::new().italic()),
856 );
857
858 let styles = theme.resolve_styles(Some(ColorMode::Light));
859 assert!(styles.has("panel"));
860 }
863
864 #[test]
865 fn test_theme_resolve_styles_dark_mode() {
866 let theme = Theme::new().add_adaptive(
867 "panel",
868 Style::new().dim(),
869 Some(Style::new().bold()),
870 Some(Style::new().italic()),
871 );
872
873 let styles = theme.resolve_styles(Some(ColorMode::Dark));
874 assert!(styles.has("panel"));
875 }
876
877 #[test]
878 fn test_theme_resolve_styles_preserves_aliases() {
879 let theme = Theme::new()
880 .add("base", Style::new().dim())
881 .add("alias", "base");
882
883 let styles = theme.resolve_styles(Some(ColorMode::Light));
884 assert!(styles.has("base"));
885 assert!(styles.has("alias"));
886
887 assert!(styles.validate().is_ok());
889 }
890
891 #[test]
896 fn test_theme_from_yaml_simple() {
897 let theme = Theme::from_yaml(
898 r#"
899 header:
900 fg: cyan
901 bold: true
902 "#,
903 )
904 .unwrap();
905
906 assert_eq!(theme.len(), 1);
907 let styles = theme.resolve_styles(None);
908 assert!(styles.has("header"));
909 }
910
911 #[test]
912 fn test_theme_from_yaml_shorthand() {
913 let theme = Theme::from_yaml(
914 r#"
915 bold_text: bold
916 accent: cyan
917 warning: "yellow italic"
918 "#,
919 )
920 .unwrap();
921
922 assert_eq!(theme.len(), 3);
923 }
924
925 #[test]
926 fn test_theme_from_yaml_alias() {
927 let theme = Theme::from_yaml(
928 r#"
929 muted:
930 dim: true
931 disabled: muted
932 "#,
933 )
934 .unwrap();
935
936 assert_eq!(theme.len(), 2);
937 assert!(theme.validate().is_ok());
938 }
939
940 #[test]
941 fn test_theme_from_yaml_adaptive() {
942 let theme = Theme::from_yaml(
943 r#"
944 panel:
945 fg: gray
946 light:
947 fg: black
948 dark:
949 fg: white
950 "#,
951 )
952 .unwrap();
953
954 assert_eq!(theme.len(), 1);
955 assert_eq!(theme.light_override_count(), 1);
956 assert_eq!(theme.dark_override_count(), 1);
957 }
958
959 #[test]
960 fn test_theme_from_yaml_invalid() {
961 let result = Theme::from_yaml("not valid yaml: [");
962 assert!(result.is_err());
963 }
964
965 #[test]
966 fn test_theme_from_yaml_complete() {
967 let theme = Theme::from_yaml(
968 r##"
969 # Visual layer
970 muted:
971 dim: true
972
973 accent:
974 fg: cyan
975 bold: true
976
977 # Adaptive
978 background:
979 light:
980 bg: "#f8f8f8"
981 dark:
982 bg: "#1e1e1e"
983
984 # Aliases
985 header: accent
986 footer: muted
987 "##,
988 )
989 .unwrap();
990
991 assert_eq!(theme.len(), 5);
993 assert!(theme.validate().is_ok());
994
995 assert_eq!(theme.light_override_count(), 1);
997 assert_eq!(theme.dark_override_count(), 1);
998 }
999
1000 #[test]
1005 fn test_theme_named() {
1006 let theme = Theme::named("darcula");
1007 assert_eq!(theme.name(), Some("darcula"));
1008 assert!(theme.is_empty());
1009 }
1010
1011 #[test]
1012 fn test_theme_new_has_no_name() {
1013 let theme = Theme::new();
1014 assert_eq!(theme.name(), None);
1015 assert_eq!(theme.source_path(), None);
1016 }
1017
1018 #[test]
1019 fn test_theme_from_file() {
1020 use std::fs;
1021 use tempfile::TempDir;
1022
1023 let temp_dir = TempDir::new().unwrap();
1024 let theme_path = temp_dir.path().join("darcula.yaml");
1025 fs::write(
1026 &theme_path,
1027 r#"
1028 header:
1029 fg: cyan
1030 bold: true
1031 muted:
1032 dim: true
1033 "#,
1034 )
1035 .unwrap();
1036
1037 let theme = Theme::from_file(&theme_path).unwrap();
1038 assert_eq!(theme.name(), Some("darcula"));
1039 assert_eq!(theme.source_path(), Some(theme_path.as_path()));
1040 assert_eq!(theme.len(), 2);
1041 }
1042
1043 #[test]
1044 fn test_theme_from_file_not_found() {
1045 let result = Theme::from_file("/nonexistent/path/theme.yaml");
1046 assert!(result.is_err());
1047 }
1048
1049 #[test]
1050 fn test_theme_refresh() {
1051 use std::fs;
1052 use tempfile::TempDir;
1053
1054 let temp_dir = TempDir::new().unwrap();
1055 let theme_path = temp_dir.path().join("dynamic.yaml");
1056 fs::write(
1057 &theme_path,
1058 r#"
1059 header:
1060 fg: red
1061 "#,
1062 )
1063 .unwrap();
1064
1065 let mut theme = Theme::from_file(&theme_path).unwrap();
1066 assert_eq!(theme.len(), 1);
1067
1068 fs::write(
1070 &theme_path,
1071 r#"
1072 header:
1073 fg: blue
1074 footer:
1075 dim: true
1076 "#,
1077 )
1078 .unwrap();
1079
1080 theme.refresh().unwrap();
1082 assert_eq!(theme.len(), 2);
1083 }
1084
1085 #[test]
1086 fn test_theme_refresh_without_source() {
1087 let mut theme = Theme::new();
1088 let result = theme.refresh();
1089 assert!(result.is_err());
1090 }
1091
1092 #[test]
1093 fn test_theme_merge() {
1094 let base = Theme::new()
1095 .add("keep", Style::new().dim())
1096 .add("overwrite", Style::new().red());
1097
1098 let extension = Theme::new()
1099 .add("overwrite", Style::new().blue())
1100 .add("new", Style::new().bold());
1101
1102 let merged = base.merge(extension);
1103
1104 let styles = merged.resolve_styles(None);
1105
1106 assert!(styles.has("keep"));
1108
1109 assert!(styles.has("overwrite"));
1111
1112 assert!(styles.has("new"));
1114
1115 assert_eq!(merged.len(), 3);
1116 }
1117
1118 #[test]
1123 fn test_theme_add_icon() {
1124 let theme = Theme::new()
1125 .add_icon("pending", IconDefinition::new("⚪"))
1126 .add_icon("done", IconDefinition::new("⚫").with_nerdfont("\u{f00c}"));
1127
1128 assert_eq!(theme.icons().len(), 2);
1129 assert!(!theme.icons().is_empty());
1130 }
1131
1132 #[test]
1133 fn test_theme_resolve_icons_classic() {
1134 let theme = Theme::new()
1135 .add_icon("pending", IconDefinition::new("⚪"))
1136 .add_icon("done", IconDefinition::new("⚫").with_nerdfont("\u{f00c}"));
1137
1138 let resolved = theme.resolve_icons(IconMode::Classic);
1139 assert_eq!(resolved.get("pending").unwrap(), "⚪");
1140 assert_eq!(resolved.get("done").unwrap(), "⚫");
1141 }
1142
1143 #[test]
1144 fn test_theme_resolve_icons_nerdfont() {
1145 let theme = Theme::new()
1146 .add_icon("pending", IconDefinition::new("⚪"))
1147 .add_icon("done", IconDefinition::new("⚫").with_nerdfont("\u{f00c}"));
1148
1149 let resolved = theme.resolve_icons(IconMode::NerdFont);
1150 assert_eq!(resolved.get("pending").unwrap(), "⚪"); assert_eq!(resolved.get("done").unwrap(), "\u{f00c}");
1152 }
1153
1154 #[test]
1155 fn test_theme_icons_empty_by_default() {
1156 let theme = Theme::new();
1157 assert!(theme.icons().is_empty());
1158 }
1159
1160 #[test]
1161 fn test_theme_merge_with_icons() {
1162 let base = Theme::new()
1163 .add_icon("keep", IconDefinition::new("K"))
1164 .add_icon("override", IconDefinition::new("OLD"));
1165
1166 let ext = Theme::new()
1167 .add_icon("override", IconDefinition::new("NEW"))
1168 .add_icon("added", IconDefinition::new("A"));
1169
1170 let merged = base.merge(ext);
1171 assert_eq!(merged.icons().len(), 3);
1172
1173 let resolved = merged.resolve_icons(IconMode::Classic);
1174 assert_eq!(resolved.get("keep").unwrap(), "K");
1175 assert_eq!(resolved.get("override").unwrap(), "NEW");
1176 assert_eq!(resolved.get("added").unwrap(), "A");
1177 }
1178
1179 #[test]
1180 fn test_theme_from_yaml_with_icons() {
1181 let theme = Theme::from_yaml(
1182 r#"
1183 header:
1184 fg: cyan
1185 bold: true
1186 icons:
1187 pending: "⚪"
1188 done:
1189 classic: "⚫"
1190 nerdfont: "nf_done"
1191 "#,
1192 )
1193 .unwrap();
1194
1195 assert_eq!(theme.len(), 1);
1197 let styles = theme.resolve_styles(None);
1198 assert!(styles.has("header"));
1199
1200 assert_eq!(theme.icons().len(), 2);
1202 let resolved = theme.resolve_icons(IconMode::Classic);
1203 assert_eq!(resolved.get("pending").unwrap(), "⚪");
1204 assert_eq!(resolved.get("done").unwrap(), "⚫");
1205
1206 let resolved = theme.resolve_icons(IconMode::NerdFont);
1207 assert_eq!(resolved.get("done").unwrap(), "nf_done");
1208 }
1209
1210 #[test]
1211 fn test_theme_from_yaml_no_icons() {
1212 let theme = Theme::from_yaml(
1213 r#"
1214 header:
1215 fg: cyan
1216 "#,
1217 )
1218 .unwrap();
1219
1220 assert!(theme.icons().is_empty());
1221 }
1222
1223 #[test]
1224 fn test_theme_from_yaml_icons_only() {
1225 let theme = Theme::from_yaml(
1226 r#"
1227 icons:
1228 check: "✓"
1229 "#,
1230 )
1231 .unwrap();
1232
1233 assert_eq!(theme.icons().len(), 1);
1234 assert_eq!(theme.len(), 0); }
1236
1237 #[test]
1238 fn test_theme_from_yaml_icons_invalid_type() {
1239 let result = Theme::from_yaml(
1240 r#"
1241 icons:
1242 bad: 42
1243 "#,
1244 );
1245 assert!(result.is_err());
1246 }
1247
1248 #[test]
1249 fn test_theme_from_yaml_icons_mapping_without_classic() {
1250 let result = Theme::from_yaml(
1251 r#"
1252 icons:
1253 bad:
1254 nerdfont: "nf"
1255 "#,
1256 );
1257 assert!(result.is_err());
1258 }
1259
1260 #[test]
1261 fn test_theme_from_file_with_icons() {
1262 use std::fs;
1263 use tempfile::TempDir;
1264
1265 let temp_dir = TempDir::new().unwrap();
1266 let theme_path = temp_dir.path().join("iconic.yaml");
1267 fs::write(
1268 &theme_path,
1269 r#"
1270 header:
1271 fg: cyan
1272 icons:
1273 check:
1274 classic: "[ok]"
1275 nerdfont: "nf_check"
1276 "#,
1277 )
1278 .unwrap();
1279
1280 let theme = Theme::from_file(&theme_path).unwrap();
1281 assert_eq!(theme.icons().len(), 1);
1282 let resolved = theme.resolve_icons(IconMode::NerdFont);
1283 assert_eq!(resolved.get("check").unwrap(), "nf_check");
1284 }
1285
1286 #[test]
1287 fn test_theme_refresh_with_icons() {
1288 use std::fs;
1289 use tempfile::TempDir;
1290
1291 let temp_dir = TempDir::new().unwrap();
1292 let theme_path = temp_dir.path().join("refresh.yaml");
1293 fs::write(
1294 &theme_path,
1295 r#"
1296 icons:
1297 v1: "one"
1298 "#,
1299 )
1300 .unwrap();
1301
1302 let mut theme = Theme::from_file(&theme_path).unwrap();
1303 assert_eq!(theme.icons().len(), 1);
1304
1305 fs::write(
1306 &theme_path,
1307 r#"
1308 icons:
1309 v1: "one"
1310 v2: "two"
1311 "#,
1312 )
1313 .unwrap();
1314
1315 theme.refresh().unwrap();
1316 assert_eq!(theme.icons().len(), 2);
1317 }
1318}