1use std::collections::HashMap;
28use std::fs::File;
29use std::io::{self, BufRead, BufReader, Cursor};
30use std::path::Path;
31
32use once_cell::sync::Lazy;
33
34use crate::color::SimpleColor as Color;
35use crate::style::Style;
36
37const DRACULA_THEME_DATA: &str = include_str!("themes/dracula.theme");
43
44const GRUVBOX_DARK_THEME_DATA: &str = include_str!("themes/gruvbox-dark.theme");
46
47const NORD_THEME_DATA: &str = include_str!("themes/nord.theme");
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum ThemeError {
53 PopBaseTheme,
55 IoError(String),
57 InvalidFormat(String),
59}
60
61impl std::fmt::Display for ThemeError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 ThemeError::PopBaseTheme => write!(f, "Unable to pop base theme"),
65 ThemeError::IoError(msg) => write!(f, "IO error: {}", msg),
66 ThemeError::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
67 }
68 }
69}
70
71impl std::error::Error for ThemeError {}
72
73impl From<io::Error> for ThemeError {
74 fn from(err: io::Error) -> Self {
75 ThemeError::IoError(err.to_string())
76 }
77}
78
79#[derive(Debug, Clone)]
84pub struct Theme {
85 styles: HashMap<String, Style>,
87 inherit: bool,
89}
90
91impl Default for Theme {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl Theme {
98 pub fn new() -> Self {
100 Theme {
101 styles: default_styles(),
102 inherit: true,
103 }
104 }
105
106 pub fn empty() -> Self {
108 Theme {
109 styles: HashMap::new(),
110 inherit: false,
111 }
112 }
113
114 pub fn with_styles(styles: HashMap<String, Style>, inherit: bool) -> Self {
116 let mut theme_styles = if inherit {
117 default_styles()
118 } else {
119 HashMap::new()
120 };
121 theme_styles.extend(styles);
122 Theme {
123 styles: theme_styles,
124 inherit,
125 }
126 }
127
128 pub fn read<P: AsRef<Path>>(path: P, inherit: bool) -> Result<Self, ThemeError> {
138 let file = File::open(path)?;
139 let reader = BufReader::new(file);
140 Self::from_reader(reader, inherit)
141 }
142
143 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ThemeError> {
145 Self::read(path, true)
146 }
147
148 pub fn from_reader<R: BufRead>(reader: R, inherit: bool) -> Result<Self, ThemeError> {
150 let mut styles = HashMap::new();
151 let mut in_styles_section = false;
152
153 for line in reader.lines() {
154 let line = line?;
155 let line = line.trim();
156
157 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
159 continue;
160 }
161
162 if line.starts_with('[') && line.ends_with(']') {
164 let section = &line[1..line.len() - 1].trim().to_lowercase();
165 in_styles_section = section == "styles";
166 continue;
167 }
168
169 if in_styles_section && let Some((name, style_str)) = line.split_once('=') {
171 let name = name.trim().to_string();
172 let style_str = style_str.trim();
173 if let Some(style) = Style::parse(style_str) {
174 styles.insert(name, style);
175 }
176 }
177 }
178
179 Ok(Self::with_styles(styles, inherit))
180 }
181
182 pub fn get_style(&self, name: &str) -> Option<Style> {
184 self.styles.get(name).copied()
185 }
186
187 pub fn add_style(&mut self, name: impl Into<String>, style: Style) {
189 self.styles.insert(name.into(), style);
190 }
191
192 pub fn remove_style(&mut self, name: &str) -> Option<Style> {
194 self.styles.remove(name)
195 }
196
197 pub fn has_style(&self, name: &str) -> bool {
199 self.styles.contains_key(name)
200 }
201
202 pub fn style_names(&self) -> impl Iterator<Item = &str> {
204 self.styles.keys().map(String::as_str)
205 }
206
207 pub fn len(&self) -> usize {
209 self.styles.len()
210 }
211
212 pub fn is_empty(&self) -> bool {
214 self.styles.is_empty()
215 }
216
217 pub fn inherits(&self) -> bool {
219 self.inherit
220 }
221
222 pub fn to_config(&self) -> String {
224 let mut lines = vec!["[styles]".to_string()];
225 let mut names: Vec<_> = self.styles.keys().collect();
226 names.sort();
227
228 for name in names {
229 if let Some(style) = self.styles.get(name) {
230 lines.push(format!("{} = {}", name, style.to_markup_string()));
231 }
232 }
233
234 lines.join("\n")
235 }
236
237 pub fn from_name(name: &str) -> Option<Self> {
254 match name.to_lowercase().as_str() {
255 "default" => Some(Self::new()),
256 "dracula" => {
257 let reader = Cursor::new(DRACULA_THEME_DATA);
258 Self::from_reader(reader, true).ok()
259 }
260 "gruvbox-dark" | "gruvbox" => {
261 let reader = Cursor::new(GRUVBOX_DARK_THEME_DATA);
262 Self::from_reader(reader, true).ok()
263 }
264 "nord" => {
265 let reader = Cursor::new(NORD_THEME_DATA);
266 Self::from_reader(reader, true).ok()
267 }
268 _ => None,
269 }
270 }
271
272 pub fn available_themes() -> Vec<&'static str> {
283 vec!["default", "dracula", "gruvbox-dark", "nord"]
284 }
285}
286
287#[derive(Debug, Clone)]
293pub struct ThemeStack {
294 entries: Vec<HashMap<String, Style>>,
296}
297
298impl ThemeStack {
299 pub fn new(theme: Theme) -> Self {
301 ThemeStack {
302 entries: vec![theme.styles],
303 }
304 }
305
306 pub fn push(&mut self, theme: Theme, inherit: bool) {
311 let styles = if inherit && !self.entries.is_empty() {
312 let mut merged = self.entries.last().unwrap().clone();
313 merged.extend(theme.styles);
314 merged
315 } else {
316 theme.styles
317 };
318 self.entries.push(styles);
319 }
320
321 pub fn push_theme(&mut self, theme: Theme) {
323 self.push(theme, true);
324 }
325
326 pub fn pop(&mut self) -> Result<(), ThemeError> {
330 if self.entries.len() == 1 {
331 return Err(ThemeError::PopBaseTheme);
332 }
333 self.entries.pop();
334 Ok(())
335 }
336
337 pub fn pop_theme(&mut self) -> Result<(), ThemeError> {
339 self.pop()
340 }
341
342 pub fn get_style(&self, name: &str) -> Option<Style> {
344 self.entries
345 .last()
346 .and_then(|styles| styles.get(name).copied())
347 }
348
349 pub fn depth(&self) -> usize {
351 self.entries.len()
352 }
353}
354
355impl Default for ThemeStack {
356 fn default() -> Self {
357 Self::new(Theme::new())
358 }
359}
360
361pub fn default_styles() -> HashMap<String, Style> {
369 let mut styles = HashMap::new();
370
371 macro_rules! add {
373 ($name:expr, $style:expr) => {
374 styles.insert($name.to_string(), $style);
375 };
376 }
377
378 add!("none", Style::new());
380 add!(
381 "reset",
382 Style {
383 color: Some(Color::Default),
384 bgcolor: Some(Color::Default),
385 bold: Some(false),
386 dim: Some(false),
387 italic: Some(false),
388 underline: Some(false),
389 blink: Some(false),
390 blink2: Some(false),
391 reverse: Some(false),
392 conceal: Some(false),
393 strike: Some(false),
394 underline2: Some(false),
395 frame: Some(false),
396 encircle: Some(false),
397 overline: Some(false),
398 }
399 );
400 add!("dim", Style::new().with_dim(true));
401 add!(
402 "bright",
403 Style {
404 dim: Some(false),
405 ..Default::default()
406 }
407 );
408 add!("bold", Style::new().with_bold(true));
409 add!("strong", Style::new().with_bold(true));
410 add!(
411 "code",
412 Style {
413 reverse: Some(true),
414 bold: Some(true),
415 ..Default::default()
416 }
417 );
418 add!("italic", Style::new().with_italic(true));
419 add!("emphasize", Style::new().with_italic(true));
420 add!("underline", Style::new().with_underline(true));
421 add!(
422 "blink",
423 Style {
424 blink: Some(true),
425 ..Default::default()
426 }
427 );
428 add!(
429 "reverse",
430 Style {
431 reverse: Some(true),
432 ..Default::default()
433 }
434 );
435 add!("strike", Style::new().with_strike(true));
436
437 add!("black", Style::color(Color::Standard(0)));
439 add!("red", Style::color(Color::Standard(1)));
440 add!("green", Style::color(Color::Standard(2)));
441 add!("yellow", Style::color(Color::Standard(3)));
442 add!("blue", Style::color(Color::Standard(4)));
443 add!("magenta", Style::color(Color::Standard(5)));
444 add!("cyan", Style::color(Color::Standard(6)));
445 add!("white", Style::color(Color::Standard(7)));
446
447 add!(
449 "inspect.attr",
450 Style::color(Color::Standard(3)).with_italic(true)
451 );
452 add!(
453 "inspect.attr.dunder",
454 Style::color(Color::Standard(3))
455 .with_italic(true)
456 .with_dim(true)
457 );
458 add!(
459 "inspect.callable",
460 Style::color(Color::Standard(1)).with_bold(true)
461 );
462 add!(
463 "inspect.async_def",
464 Style::color(Color::Standard(14)).with_italic(true)
465 );
466 add!(
467 "inspect.def",
468 Style::color(Color::Standard(14)).with_italic(true)
469 );
470 add!(
471 "inspect.class",
472 Style::color(Color::Standard(14)).with_italic(true)
473 );
474 add!(
475 "inspect.error",
476 Style::color(Color::Standard(1)).with_bold(true)
477 );
478 add!("inspect.equals", Style::new());
479 add!("inspect.help", Style::color(Color::Standard(6)));
480 add!("inspect.doc", Style::new().with_dim(true));
481 add!("inspect.value.border", Style::color(Color::Standard(2)));
482
483 add!(
485 "live.ellipsis",
486 Style::color(Color::Standard(1)).with_bold(true)
487 );
488
489 add!(
491 "layout.tree.row",
492 Style::color(Color::Standard(1)).with_dim(false)
493 );
494 add!(
495 "layout.tree.column",
496 Style::color(Color::Standard(4)).with_dim(false)
497 );
498
499 add!(
501 "logging.keyword",
502 Style::color(Color::Standard(3)).with_bold(true)
503 );
504 add!("logging.level.notset", Style::new().with_dim(true));
505 add!("logging.level.debug", Style::color(Color::Standard(2)));
506 add!("logging.level.info", Style::color(Color::Standard(4)));
507 add!("logging.level.warning", Style::color(Color::Standard(3)));
508 add!(
509 "logging.level.error",
510 Style::color(Color::Standard(1)).with_bold(true)
511 );
512 add!(
513 "logging.level.critical",
514 Style {
515 color: Some(Color::Standard(1)),
516 bold: Some(true),
517 reverse: Some(true),
518 ..Default::default()
519 }
520 );
521
522 add!("log.level", Style::new());
524 add!("log.time", Style::color(Color::Standard(6)).with_dim(true));
525 add!("log.message", Style::new());
526 add!("log.path", Style::new().with_dim(true));
527
528 add!("repr.ellipsis", Style::color(Color::Standard(3)));
530 add!(
531 "repr.indent",
532 Style::color(Color::Standard(2)).with_dim(true)
533 );
534 add!(
535 "repr.error",
536 Style::color(Color::Standard(1)).with_bold(true)
537 );
538 add!(
539 "repr.str",
540 Style::color(Color::Standard(2))
541 .with_italic(false)
542 .with_bold(false)
543 );
544 add!("repr.brace", Style::new().with_bold(true));
545 add!("repr.comma", Style::new().with_bold(true));
546 add!(
547 "repr.ipv4",
548 Style::color(Color::Standard(10)).with_bold(true)
549 );
550 add!(
551 "repr.ipv6",
552 Style::color(Color::Standard(10)).with_bold(true)
553 );
554 add!(
555 "repr.eui48",
556 Style::color(Color::Standard(10)).with_bold(true)
557 );
558 add!(
559 "repr.eui64",
560 Style::color(Color::Standard(10)).with_bold(true)
561 );
562 add!("repr.tag_start", Style::new().with_bold(true));
563 add!(
564 "repr.tag_name",
565 Style::color(Color::Standard(13)).with_bold(true)
566 );
567 add!("repr.tag_contents", Style::color(Color::Default));
568 add!("repr.tag_end", Style::new().with_bold(true));
569 add!(
570 "repr.attrib_name",
571 Style::color(Color::Standard(3)).with_italic(false)
572 );
573 add!("repr.attrib_equal", Style::new().with_bold(true));
574 add!(
575 "repr.attrib_value",
576 Style::color(Color::Standard(5)).with_italic(false)
577 );
578 add!(
579 "repr.number",
580 Style::color(Color::Standard(6))
581 .with_bold(true)
582 .with_italic(false)
583 );
584 add!(
585 "repr.number_complex",
586 Style::color(Color::Standard(6))
587 .with_bold(true)
588 .with_italic(false)
589 );
590 add!(
591 "repr.bool_true",
592 Style::color(Color::Standard(10)).with_italic(true)
593 );
594 add!(
595 "repr.bool_false",
596 Style::color(Color::Standard(9)).with_italic(true)
597 );
598 add!(
599 "repr.none",
600 Style::color(Color::Standard(5)).with_italic(true)
601 );
602 add!(
603 "repr.url",
604 Style::color(Color::Standard(12))
605 .with_underline(true)
606 .with_italic(false)
607 .with_bold(false)
608 );
609 add!(
610 "repr.uuid",
611 Style::color(Color::Standard(11)).with_bold(false)
612 );
613 add!(
614 "repr.call",
615 Style::color(Color::Standard(5)).with_bold(true)
616 );
617 add!("repr.path", Style::color(Color::Standard(5)));
618 add!("repr.filename", Style::color(Color::Standard(13)));
619
620 add!("rule.line", Style::color(Color::Standard(10)));
622 add!("rule.text", Style::new());
623
624 add!("json.brace", Style::new().with_bold(true));
626 add!(
627 "json.bool_true",
628 Style::color(Color::Standard(10)).with_italic(true)
629 );
630 add!(
631 "json.bool_false",
632 Style::color(Color::Standard(9)).with_italic(true)
633 );
634 add!(
635 "json.null",
636 Style::color(Color::Standard(5)).with_italic(true)
637 );
638 add!(
639 "json.number",
640 Style::color(Color::Standard(6))
641 .with_bold(true)
642 .with_italic(false)
643 );
644 add!(
645 "json.str",
646 Style::color(Color::Standard(2))
647 .with_italic(false)
648 .with_bold(false)
649 );
650 add!("json.key", Style::color(Color::Standard(4)).with_bold(true));
651
652 add!("prompt", Style::new());
654 add!(
655 "prompt.choices",
656 Style::color(Color::Standard(5)).with_bold(true)
657 );
658 add!(
659 "prompt.default",
660 Style::color(Color::Standard(6)).with_bold(true)
661 );
662 add!("prompt.invalid", Style::color(Color::Standard(1)));
663 add!("prompt.invalid.choice", Style::color(Color::Standard(1)));
664
665 add!("pretty", Style::new());
667
668 add!(
670 "scope.border",
671 Style::color(Color::Standard(4)).with_dim(true)
672 );
673 add!(
674 "scope.key",
675 Style::color(Color::Standard(6)).with_bold(true)
676 );
677 add!(
678 "scope.key.special",
679 Style::color(Color::Standard(3))
680 .with_italic(true)
681 .with_dim(true)
682 );
683 add!("scope.equals", Style::new());
684
685 add!("table.header", Style::new().with_bold(true));
687 add!("table.footer", Style::new().with_bold(true));
688 add!("table.cell", Style::new());
689 add!("table.title", Style::new().with_italic(true));
690 add!(
691 "table.caption",
692 Style::new().with_italic(true).with_dim(true)
693 );
694
695 add!("traceback.border", Style::color(Color::Standard(1)));
697 add!(
698 "traceback.border.syntax_error",
699 Style::color(Color::Standard(9))
700 );
701 add!(
702 "traceback.title",
703 Style::color(Color::Standard(1)).with_bold(true)
704 );
705 add!("traceback.text", Style::color(Color::Standard(1)));
706 add!(
707 "traceback.exc_type",
708 Style::color(Color::Standard(9)).with_bold(true)
709 );
710 add!("traceback.exc_value", Style::new());
711 add!(
712 "traceback.error",
713 Style::color(Color::Standard(1)).with_bold(true)
714 );
715 add!(
716 "traceback.error_range",
717 Style::color(Color::Standard(1))
718 .with_bold(true)
719 .with_underline(true)
720 );
721 add!("traceback.path", Style::color(Color::Standard(8)));
722 add!("traceback.filename", Style::color(Color::Standard(13)));
723 add!("traceback.lineno", Style::color(Color::Standard(13)));
724 add!(
725 "traceback.offset",
726 Style::color(Color::Standard(9)).with_bold(true)
727 );
728 add!(
729 "traceback.note",
730 Style::color(Color::Standard(2)).with_bold(true)
731 );
732 add!("traceback.group.border", Style::color(Color::Standard(5)));
733
734 add!("bar.back", Style::color(Color::EightBit(59))); add!(
737 "bar.complete",
738 Style::color(Color::Rgb {
739 r: 249,
740 g: 38,
741 b: 114
742 })
743 );
744 add!(
745 "bar.finished",
746 Style::color(Color::Rgb {
747 r: 114,
748 g: 156,
749 b: 31
750 })
751 );
752 add!(
753 "bar.pulse",
754 Style::color(Color::Rgb {
755 r: 249,
756 g: 38,
757 b: 114
758 })
759 );
760
761 add!("progress.description", Style::new());
762 add!("progress.filesize", Style::color(Color::Standard(2)));
763 add!("progress.filesize.total", Style::color(Color::Standard(2)));
764 add!("progress.download", Style::color(Color::Standard(2)));
765 add!("progress.elapsed", Style::color(Color::Standard(3)));
766 add!("progress.percentage", Style::color(Color::Standard(5)));
767 add!("progress.remaining", Style::color(Color::Standard(6)));
768 add!("progress.data.speed", Style::color(Color::Standard(1)));
769 add!("progress.spinner", Style::color(Color::Standard(2)));
770
771 add!("status.spinner", Style::color(Color::Standard(2)));
772
773 add!("tree", Style::new());
775 add!("tree.line", Style::new());
776
777 add!("markdown.paragraph", Style::new());
779 add!("markdown.text", Style::new());
780 add!("markdown.em", Style::new().with_italic(true));
781 add!("markdown.emph", Style::new().with_italic(true));
782 add!("markdown.strong", Style::new().with_bold(true));
783 add!(
784 "markdown.code",
785 Style::color(Color::Standard(6))
786 .with_bold(true)
787 .with_bgcolor(Color::Standard(0))
788 );
789 add!(
790 "markdown.code_block",
791 Style::color(Color::Standard(6)).with_bgcolor(Color::Standard(0))
792 );
793 add!("markdown.block_quote", Style::color(Color::Standard(5)));
794 add!("markdown.list", Style::color(Color::Standard(6)));
795 add!("markdown.item", Style::new());
796 add!(
797 "markdown.item.bullet",
798 Style::color(Color::Standard(3)).with_bold(true)
799 );
800 add!(
801 "markdown.item.number",
802 Style::color(Color::Standard(3)).with_bold(true)
803 );
804 add!("markdown.hr", Style::color(Color::Standard(3)));
805 add!("markdown.h1.border", Style::new());
806 add!("markdown.h1", Style::new().with_bold(true));
807 add!(
808 "markdown.h2",
809 Style::new().with_bold(true).with_underline(true)
810 );
811 add!("markdown.h3", Style::new().with_bold(true));
812 add!("markdown.h4", Style::new().with_bold(true).with_dim(true));
813 add!("markdown.h5", Style::new().with_underline(true));
814 add!("markdown.h6", Style::new().with_italic(true));
815 add!("markdown.h7", Style::new().with_italic(true).with_dim(true));
816 add!("markdown.link", Style::color(Color::Standard(12)));
817 add!(
818 "markdown.link_url",
819 Style::color(Color::Standard(4)).with_underline(true)
820 );
821 add!("markdown.s", Style::new().with_strike(true));
822 add!("markdown.table.border", Style::color(Color::Standard(6)));
823 add!(
824 "markdown.table.header",
825 Style::color(Color::Standard(6)).with_bold(false)
826 );
827
828 add!(
830 "blink2",
831 Style {
832 blink2: Some(true),
833 ..Default::default()
834 }
835 );
836
837 add!("iso8601.date", Style::color(Color::Standard(4)));
839 add!("iso8601.time", Style::color(Color::Standard(5)));
840 add!("iso8601.timezone", Style::color(Color::Standard(3)));
841
842 styles
843}
844
845static DEFAULT_STYLES_MAP: Lazy<HashMap<String, Style>> = Lazy::new(default_styles);
846
847pub fn get_default_style(name: &str) -> Option<Style> {
851 DEFAULT_STYLES_MAP.get(name).copied()
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 #[test]
859 fn test_theme_new() {
860 let theme = Theme::new();
861 assert!(theme.has_style("repr.number"));
863 assert!(theme.has_style("markdown.h1"));
864 assert!(theme.len() > 50);
865 }
866
867 #[test]
868 fn test_theme_empty() {
869 let theme = Theme::empty();
870 assert!(theme.is_empty());
871 assert!(!theme.has_style("repr.number"));
872 }
873
874 #[test]
875 fn test_theme_get_style() {
876 let theme = Theme::new();
877 let style = theme.get_style("repr.number");
878 assert!(style.is_some());
879 let style = style.unwrap();
880 assert_eq!(style.bold, Some(true));
881 assert_eq!(style.color, Some(Color::Standard(6)));
882 }
883
884 #[test]
885 fn test_theme_add_style() {
886 let mut theme = Theme::empty();
887 theme.add_style("custom", Style::new().with_bold(true));
888 assert!(theme.has_style("custom"));
889 assert_eq!(theme.get_style("custom").unwrap().bold, Some(true));
890 }
891
892 #[test]
893 fn test_theme_remove_style() {
894 let mut theme = Theme::new();
895 assert!(theme.has_style("repr.number"));
896 theme.remove_style("repr.number");
897 assert!(!theme.has_style("repr.number"));
898 }
899
900 #[test]
901 fn test_theme_with_styles() {
902 let mut custom = HashMap::new();
903 custom.insert("my.style".to_string(), Style::new().with_italic(true));
904
905 let theme = Theme::with_styles(custom, true);
906 assert!(theme.has_style("repr.number"));
908 assert!(theme.has_style("my.style"));
909 }
910
911 #[test]
912 fn test_theme_with_styles_no_inherit() {
913 let mut custom = HashMap::new();
914 custom.insert("my.style".to_string(), Style::new().with_italic(true));
915
916 let theme = Theme::with_styles(custom, false);
917 assert!(!theme.has_style("repr.number"));
919 assert!(theme.has_style("my.style"));
920 }
921
922 #[test]
923 fn test_theme_to_config() {
924 let mut theme = Theme::empty();
925 theme.add_style("test.bold", Style::new().with_bold(true));
926 theme.add_style("test.color", Style::color(Color::Standard(1)));
927
928 let config = theme.to_config();
929 assert!(config.starts_with("[styles]"));
930 assert!(config.contains("test.bold = bold"));
931 assert!(config.contains("test.color = red"));
932 }
933
934 #[test]
935 fn test_theme_from_reader() {
936 let config = r#"
937[styles]
938custom.style = bold red
939another = italic cyan
940"#;
941 let reader = std::io::Cursor::new(config);
942 let theme = Theme::from_reader(reader, false).unwrap();
943
944 assert!(theme.has_style("custom.style"));
945 let style = theme.get_style("custom.style").unwrap();
946 assert_eq!(style.bold, Some(true));
947 assert_eq!(style.color, Some(Color::Standard(1)));
948
949 let another = theme.get_style("another").unwrap();
950 assert_eq!(another.italic, Some(true));
951 assert_eq!(another.color, Some(Color::Standard(6)));
952 }
953
954 #[test]
955 fn test_theme_from_reader_with_comments() {
956 let config = r#"
957# Comment line
958[styles]
959; Another comment
960test = bold
961"#;
962 let reader = std::io::Cursor::new(config);
963 let theme = Theme::from_reader(reader, false).unwrap();
964 assert!(theme.has_style("test"));
965 }
966
967 #[test]
968 fn test_theme_stack_new() {
969 let stack = ThemeStack::new(Theme::new());
970 assert_eq!(stack.depth(), 1);
971 assert!(stack.get_style("repr.number").is_some());
972 }
973
974 #[test]
975 fn test_theme_stack_push_pop() {
976 let mut stack = ThemeStack::new(Theme::new());
977
978 let mut custom = Theme::empty();
980 custom.add_style("repr.number", Style::new().with_italic(true));
981
982 stack.push(custom, true);
984 assert_eq!(stack.depth(), 2);
985
986 let style = stack.get_style("repr.number").unwrap();
988 assert_eq!(style.italic, Some(true));
989
990 stack.pop().unwrap();
992 assert_eq!(stack.depth(), 1);
993
994 let style = stack.get_style("repr.number").unwrap();
995 assert_eq!(style.bold, Some(true));
996 assert_eq!(style.italic, Some(false));
998 }
999
1000 #[test]
1001 fn test_theme_stack_push_inherit() {
1002 let mut stack = ThemeStack::new(Theme::new());
1003
1004 let mut custom = Theme::empty();
1005 custom.add_style("custom.style", Style::new().with_bold(true));
1006
1007 stack.push(custom, true);
1009 assert!(stack.get_style("repr.number").is_some());
1010 assert!(stack.get_style("custom.style").is_some());
1011 }
1012
1013 #[test]
1014 fn test_theme_stack_push_no_inherit() {
1015 let mut stack = ThemeStack::new(Theme::new());
1016
1017 let mut custom = Theme::empty();
1018 custom.add_style("custom.style", Style::new().with_bold(true));
1019
1020 stack.push(custom, false);
1022 assert!(stack.get_style("repr.number").is_none());
1023 assert!(stack.get_style("custom.style").is_some());
1024 }
1025
1026 #[test]
1027 fn test_theme_stack_pop_base_error() {
1028 let mut stack = ThemeStack::new(Theme::new());
1029 let result = stack.pop();
1030 assert!(matches!(result, Err(ThemeError::PopBaseTheme)));
1031 }
1032
1033 #[test]
1034 fn test_default_styles_count() {
1035 let styles = default_styles();
1036 assert!(
1038 styles.len() >= 100,
1039 "Expected at least 100 default styles, got {}",
1040 styles.len()
1041 );
1042 }
1043
1044 #[test]
1045 fn test_default_styles_has_expected() {
1046 let styles = default_styles();
1047
1048 let expected = [
1050 "none",
1051 "reset",
1052 "bold",
1053 "italic",
1054 "repr.number",
1055 "repr.str",
1056 "repr.bool_true",
1057 "markdown.h1",
1058 "markdown.code",
1059 "log.level",
1060 "log.time",
1061 "json.brace",
1062 "json.key",
1063 "table.header",
1064 "table.cell",
1065 "traceback.error",
1066 "traceback.title",
1067 "progress.spinner",
1068 "progress.percentage",
1069 ];
1070
1071 for name in expected {
1072 assert!(styles.contains_key(name), "Missing default style: {}", name);
1073 }
1074 }
1075
1076 #[test]
1077 fn test_style_to_string_roundtrip() {
1078 let style = Style::new().with_bold(true).with_color(Color::Standard(1));
1079
1080 let s = style.to_markup_string();
1081 assert!(s.contains("bold"));
1082 assert!(s.contains("red"));
1083
1084 let parsed = Style::parse(&s).unwrap();
1085 assert_eq!(parsed.bold, Some(true));
1086 assert_eq!(parsed.color, Some(Color::Standard(1)));
1087 }
1088
1089 #[test]
1090 fn test_theme_from_name() {
1091 let default = Theme::from_name("default");
1093 assert!(default.is_some());
1094 assert!(default.unwrap().has_style("repr.number"));
1095
1096 let dracula = Theme::from_name("dracula");
1098 assert!(dracula.is_some());
1099 let dracula = dracula.unwrap();
1100 assert!(dracula.has_style("repr.number"));
1101 let style = dracula.get_style("repr.number").unwrap();
1103 assert!(style.color.is_some());
1104
1105 let gruvbox = Theme::from_name("gruvbox-dark");
1107 assert!(gruvbox.is_some());
1108
1109 let nord = Theme::from_name("nord");
1111 assert!(nord.is_some());
1112
1113 assert!(Theme::from_name("nonexistent").is_none());
1115 }
1116
1117 #[test]
1118 fn test_theme_available_themes() {
1119 let themes = Theme::available_themes();
1120 assert!(themes.contains(&"default"));
1121 assert!(themes.contains(&"dracula"));
1122 assert!(themes.contains(&"gruvbox-dark"));
1123 assert!(themes.contains(&"nord"));
1124 }
1125}