1use std::fmt::Write;
12
13mod catppuccin;
14pub mod style_syntax;
15pub mod user;
16
17pub use style_syntax::{parse_style, StyleParseError};
18pub use user::{RegisteredTheme, ThemeRegistry, ThemeSource};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Role {
25 Foreground = 0,
27 Background = 1,
28 Muted = 2,
29 Primary = 3,
30 Accent = 4,
31 Success = 5,
32 Warning = 6,
33 Error = 7,
34 Info = 8,
35 SuccessDim = 9,
38 WarningDim = 10,
39 ErrorDim = 11,
40 PrimaryDim = 12,
41 AccentDim = 13,
42 Surface = 14,
43 Border = 15,
44}
45
46const _: () = assert!(Role::Border as usize == Role::COUNT - 1);
50
51impl Role {
52 pub const COUNT: usize = 16;
55
56 #[must_use]
59 pub fn fallback(self) -> Role {
60 match self {
61 Self::SuccessDim => Self::Success,
62 Self::WarningDim => Self::Warning,
63 Self::ErrorDim => Self::Error,
64 Self::PrimaryDim => Self::Primary,
65 Self::AccentDim => Self::Accent,
66 Self::Surface => Self::Background,
67 Self::Border => Self::Muted,
68 other => other,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum AnsiColor {
77 Black = 0,
78 Red = 1,
79 Green = 2,
80 Yellow = 3,
81 Blue = 4,
82 Magenta = 5,
83 Cyan = 6,
84 White = 7,
85 BrightBlack = 8,
86 BrightRed = 9,
87 BrightGreen = 10,
88 BrightYellow = 11,
89 BrightBlue = 12,
90 BrightMagenta = 13,
91 BrightCyan = 14,
92 BrightWhite = 15,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum Color {
99 TrueColor {
100 r: u8,
101 g: u8,
102 b: u8,
103 },
104 Palette256(u8),
105 Palette16(AnsiColor),
106 NoColor,
110}
111
112impl Color {
113 #[must_use]
117 pub fn downgrade(self, cap: Capability) -> Color {
118 match (self, cap) {
119 (_, Capability::None) => Color::NoColor,
120 (Color::NoColor, _) => Color::NoColor,
121 (Color::TrueColor { r, g, b }, Capability::Palette256) => {
123 Color::Palette256(rgb_to_256(r, g, b))
124 }
125 (Color::TrueColor { r, g, b }, Capability::Palette16) => {
126 Color::Palette16(rgb_to_ansi16(r, g, b))
127 }
128 (Color::Palette256(n), Capability::Palette16) => {
130 Color::Palette16(palette256_to_ansi16(n))
131 }
132 (c, _) => c,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct Style {
145 pub role: Option<Role>,
146 pub fg: Option<Color>,
147 pub bold: bool,
148 pub italic: bool,
149 pub underline: bool,
150 pub dim: bool,
151}
152
153impl Style {
154 #[must_use]
156 pub const fn role(role: Role) -> Self {
157 Self {
158 role: Some(role),
159 fg: None,
160 bold: false,
161 italic: false,
162 underline: false,
163 dim: false,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
176pub enum Capability {
177 None,
178 Palette16,
179 Palette256,
180 TrueColor,
181}
182
183impl Capability {
184 #[must_use]
188 pub fn detect() -> Self {
189 if std::env::var_os("NO_COLOR").is_some() {
190 return Self::None;
191 }
192 Self::from_terminal()
193 }
194
195 #[must_use]
201 pub fn from_terminal() -> Self {
202 match supports_color::on(supports_color::Stream::Stdout) {
203 Some(c) if c.has_16m => Self::TrueColor,
204 Some(c) if c.has_256 => Self::Palette256,
205 Some(_) => Self::Palette16,
206 None => Self::None,
207 }
208 }
209
210 #[must_use]
220 pub(crate) fn from_env_vars(colorterm: Option<&str>, term: Option<&str>) -> Self {
221 if let Some(c) = colorterm {
222 if c.eq_ignore_ascii_case("truecolor") || c.eq_ignore_ascii_case("24bit") {
223 return Self::TrueColor;
224 }
225 }
226 match term.map(str::trim) {
227 None | Some("") => Self::None,
228 Some(t) if t.eq_ignore_ascii_case("dumb") => Self::None,
229 Some(t) if t.contains("256color") => Self::Palette256,
230 _ => Self::Palette16,
231 }
232 }
233
234 #[must_use]
242 pub(crate) fn force_from(tty: Self, env: Self) -> Self {
243 tty.max(env).max(Self::Palette16)
244 }
245}
246
247#[derive(Debug, Clone)]
254pub struct Theme {
255 name: &'static str,
256 colors: [Option<Color>; Role::COUNT],
257}
258
259impl Theme {
260 #[must_use]
264 pub fn name(&self) -> &'static str {
265 self.name
266 }
267
268 #[must_use]
273 pub(super) fn from_user_parts(
274 name: &'static str,
275 colors: [Option<Color>; Role::COUNT],
276 ) -> Self {
277 Self { name, colors }
278 }
279
280 #[must_use]
285 pub fn color(&self, role: Role) -> Color {
286 let mut current = role;
287 loop {
288 if let Some(c) = self.colors[current as usize] {
289 return c;
290 }
291 let next = current.fallback();
292 if next == current {
293 return Color::NoColor;
294 }
295 current = next;
296 }
297 }
298}
299
300const BUILTIN_THEMES: &[Theme] = &[
305 Theme {
306 name: "default",
307 colors: {
310 let mut c = [None; Role::COUNT];
311 c[Role::Foreground as usize] = Some(Color::Palette16(AnsiColor::BrightWhite));
312 c[Role::Background as usize] = Some(Color::NoColor);
313 c[Role::Muted as usize] = Some(Color::Palette16(AnsiColor::BrightBlack));
314 c[Role::Primary as usize] = Some(Color::Palette16(AnsiColor::BrightMagenta));
315 c[Role::Accent as usize] = Some(Color::Palette16(AnsiColor::BrightBlue));
316 c[Role::Success as usize] = Some(Color::Palette16(AnsiColor::BrightGreen));
317 c[Role::Warning as usize] = Some(Color::Palette16(AnsiColor::BrightYellow));
318 c[Role::Error as usize] = Some(Color::Palette16(AnsiColor::BrightRed));
319 c[Role::Info as usize] = Some(Color::Palette16(AnsiColor::BrightCyan));
320 c
321 },
322 },
323 Theme {
324 name: "minimal",
325 colors: [None; Role::COUNT],
326 },
327 catppuccin::LATTE,
328 catppuccin::FRAPPE,
329 catppuccin::MACCHIATO,
330 catppuccin::MOCHA,
331];
332
333#[must_use]
336pub fn built_in(name: &str) -> Option<&'static Theme> {
337 BUILTIN_THEMES.iter().find(|t| t.name == name)
338}
339
340#[must_use]
343pub fn default_theme() -> &'static Theme {
344 built_in("default").expect("`default` theme is always compiled in")
345}
346
347pub fn builtin_names() -> impl Iterator<Item = &'static str> {
349 BUILTIN_THEMES.iter().map(|t| t.name)
350}
351
352#[must_use]
356pub fn sgr_open(style: &Style, theme: &Theme, cap: Capability) -> String {
357 let mut params = Vec::<u16>::with_capacity(4);
358 if style.bold {
359 params.push(1);
360 }
361 if style.dim {
362 params.push(2);
363 }
364 if style.italic {
365 params.push(3);
366 }
367 if style.underline {
368 params.push(4);
369 }
370 let fg = resolve_fg(style, theme, cap);
371 emit_fg_params(fg, &mut params);
372 if params.is_empty() {
373 return String::new();
374 }
375 let mut out = String::from("\x1b[");
376 for (i, p) in params.iter().enumerate() {
377 if i > 0 {
378 out.push(';');
379 }
380 let _ = write!(out, "{p}");
381 }
382 out.push('m');
383 out
384}
385
386#[must_use]
389pub const fn sgr_reset() -> &'static str {
390 "\x1b[0m"
391}
392
393fn resolve_fg(style: &Style, theme: &Theme, cap: Capability) -> Color {
397 let raw = style
398 .fg
399 .or_else(|| style.role.map(|r| theme.color(r)))
400 .unwrap_or(Color::NoColor);
401 raw.downgrade(cap)
402}
403
404fn emit_fg_params(color: Color, out: &mut Vec<u16>) {
407 match color {
408 Color::NoColor => {}
409 Color::Palette16(ansi) => {
410 let code = ansi_to_sgr_fg(ansi);
411 out.push(code);
412 }
413 Color::Palette256(n) => {
414 out.extend_from_slice(&[38, 5, u16::from(n)]);
415 }
416 Color::TrueColor { r, g, b } => {
417 out.extend_from_slice(&[38, 2, u16::from(r), u16::from(g), u16::from(b)]);
418 }
419 }
420}
421
422fn ansi_to_sgr_fg(c: AnsiColor) -> u16 {
423 let n = c as u8;
424 if n < 8 {
425 30 + u16::from(n)
426 } else {
427 90 + u16::from(n - 8)
429 }
430}
431
432fn rgb_to_256(r: u8, g: u8, b: u8) -> u8 {
438 if r == g && g == b {
439 if r < 8 {
440 return 16;
441 }
442 if r > 238 {
446 return 231;
447 }
448 return 232 + (r - 8) / 10;
449 }
450 let r6 = scale_to_cube(r);
451 let g6 = scale_to_cube(g);
452 let b6 = scale_to_cube(b);
453 16 + 36 * r6 + 6 * g6 + b6
454}
455
456fn scale_to_cube(channel: u8) -> u8 {
457 const LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
459 let mut best = 0u8;
460 let mut best_dist = u16::MAX;
461 for (i, &level) in LEVELS.iter().enumerate() {
462 let d = u16::from(channel.abs_diff(level));
463 if d < best_dist {
464 best_dist = d;
465 best = u8::try_from(i).expect("cube index fits in u8");
466 }
467 }
468 best
469}
470
471fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> AnsiColor {
477 let max = r.max(g).max(b);
478 let min = r.min(g).min(b);
479 let bright = max >= 128;
480 if max - min < 32 {
482 return match max {
483 0..=42 => AnsiColor::Black,
484 43..=170 => {
485 if bright {
486 AnsiColor::BrightBlack
487 } else {
488 AnsiColor::White
489 }
490 }
491 _ => AnsiColor::BrightWhite,
492 };
493 }
494 let dominant_r = r >= g && r >= b;
496 let dominant_g = g >= r && g >= b;
497 let dominant_b = b >= r && b >= g;
498 let r_hi = r >= 128;
499 let g_hi = g >= 128;
500 let b_hi = b >= 128;
501 match (dominant_r, dominant_g, dominant_b, r_hi, g_hi, b_hi) {
502 (true, _, _, _, true, _) => {
503 if bright {
504 AnsiColor::BrightYellow
505 } else {
506 AnsiColor::Yellow
507 }
508 }
509 (true, _, _, _, _, true) => {
510 if bright {
511 AnsiColor::BrightMagenta
512 } else {
513 AnsiColor::Magenta
514 }
515 }
516 (_, true, _, _, _, true) => {
517 if bright {
518 AnsiColor::BrightCyan
519 } else {
520 AnsiColor::Cyan
521 }
522 }
523 (_, true, _, true, _, _) => {
524 if bright {
525 AnsiColor::BrightYellow
526 } else {
527 AnsiColor::Yellow
528 }
529 }
530 (true, _, _, _, _, _) => {
531 if bright {
532 AnsiColor::BrightRed
533 } else {
534 AnsiColor::Red
535 }
536 }
537 (_, true, _, _, _, _) => {
538 if bright {
539 AnsiColor::BrightGreen
540 } else {
541 AnsiColor::Green
542 }
543 }
544 (_, _, true, _, _, _) => {
545 if bright {
546 AnsiColor::BrightBlue
547 } else {
548 AnsiColor::Blue
549 }
550 }
551 _ => AnsiColor::White,
552 }
553}
554
555fn palette256_to_ansi16(n: u8) -> AnsiColor {
559 if n < 16 {
560 return match n {
561 0 => AnsiColor::Black,
562 1 => AnsiColor::Red,
563 2 => AnsiColor::Green,
564 3 => AnsiColor::Yellow,
565 4 => AnsiColor::Blue,
566 5 => AnsiColor::Magenta,
567 6 => AnsiColor::Cyan,
568 7 => AnsiColor::White,
569 8 => AnsiColor::BrightBlack,
570 9 => AnsiColor::BrightRed,
571 10 => AnsiColor::BrightGreen,
572 11 => AnsiColor::BrightYellow,
573 12 => AnsiColor::BrightBlue,
574 13 => AnsiColor::BrightMagenta,
575 14 => AnsiColor::BrightCyan,
576 _ => AnsiColor::BrightWhite,
577 };
578 }
579 if n >= 232 {
580 let step = n - 232;
582 return match step {
583 0..=7 => AnsiColor::Black,
584 8..=15 => AnsiColor::BrightBlack,
585 16..=19 => AnsiColor::White,
586 _ => AnsiColor::BrightWhite,
587 };
588 }
589 const LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
591 let c = n - 16;
592 let r6 = usize::from(c / 36);
593 let g6 = usize::from((c / 6) % 6);
594 let b6 = usize::from(c % 6);
595 rgb_to_ansi16(LEVELS[r6], LEVELS[g6], LEVELS[b6])
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 #[test]
605 fn role_count_matches_enum_discriminant_range() {
606 assert_eq!(Role::Foreground as usize, 0);
607 assert_eq!(Role::Border as usize, Role::COUNT - 1);
608 }
609
610 #[test]
611 fn extended_role_fallbacks_match_spec_table() {
612 assert_eq!(Role::SuccessDim.fallback(), Role::Success);
613 assert_eq!(Role::WarningDim.fallback(), Role::Warning);
614 assert_eq!(Role::ErrorDim.fallback(), Role::Error);
615 assert_eq!(Role::PrimaryDim.fallback(), Role::Primary);
616 assert_eq!(Role::AccentDim.fallback(), Role::Accent);
617 assert_eq!(Role::Surface.fallback(), Role::Background);
618 assert_eq!(Role::Border.fallback(), Role::Muted);
619 }
620
621 #[test]
622 fn base_roles_fall_back_to_themselves() {
623 for role in [
624 Role::Foreground,
625 Role::Background,
626 Role::Muted,
627 Role::Primary,
628 Role::Accent,
629 Role::Success,
630 Role::Warning,
631 Role::Error,
632 Role::Info,
633 ] {
634 assert_eq!(role.fallback(), role);
635 }
636 }
637
638 #[test]
641 fn default_theme_maps_every_base_role() {
642 let t = built_in("default").expect("default exists");
643 for role in [
644 Role::Foreground,
645 Role::Muted,
646 Role::Primary,
647 Role::Accent,
648 Role::Success,
649 Role::Warning,
650 Role::Error,
651 Role::Info,
652 ] {
653 let c = t.color(role);
654 assert!(
655 !matches!(c, Color::NoColor),
656 "default theme left {role:?} as NoColor",
657 );
658 }
659 }
660
661 #[test]
662 fn default_theme_extended_roles_fall_back_to_base() {
663 let t = built_in("default").expect("default exists");
664 assert_eq!(t.color(Role::SuccessDim), t.color(Role::Success));
665 assert_eq!(t.color(Role::PrimaryDim), t.color(Role::Primary));
666 assert_eq!(t.color(Role::Border), t.color(Role::Muted));
667 }
668
669 #[test]
670 fn minimal_theme_returns_no_color_for_every_role() {
671 let t = built_in("minimal").expect("minimal exists");
672 for role in [
673 Role::Foreground,
674 Role::Primary,
675 Role::Warning,
676 Role::SuccessDim,
677 ] {
678 assert_eq!(t.color(role), Color::NoColor);
679 }
680 }
681
682 #[test]
683 fn unknown_theme_name_returns_none() {
684 assert!(built_in("nope").is_none());
685 }
686
687 #[test]
688 fn default_theme_always_available() {
689 assert_eq!(default_theme().name, "default");
690 }
691
692 #[test]
693 fn builtin_names_lists_default_and_minimal() {
694 let names: Vec<&str> = builtin_names().collect();
695 assert!(names.contains(&"default"));
696 assert!(names.contains(&"minimal"));
697 }
698
699 #[test]
702 fn downgrade_strips_color_under_no_capability() {
703 assert_eq!(
704 Color::TrueColor { r: 255, g: 0, b: 0 }.downgrade(Capability::None),
705 Color::NoColor
706 );
707 assert_eq!(
708 Color::Palette16(AnsiColor::Red).downgrade(Capability::None),
709 Color::NoColor
710 );
711 }
712
713 #[test]
714 fn downgrade_preserves_matching_or_richer_capability() {
715 let c = Color::Palette16(AnsiColor::Green);
716 assert_eq!(c.downgrade(Capability::Palette16), c);
717 assert_eq!(c.downgrade(Capability::Palette256), c);
718 assert_eq!(c.downgrade(Capability::TrueColor), c);
719 }
720
721 #[test]
722 fn downgrade_truecolor_to_256_grayscale_uses_gray_ramp() {
723 let g = Color::TrueColor {
725 r: 128,
726 g: 128,
727 b: 128,
728 };
729 match g.downgrade(Capability::Palette256) {
730 Color::Palette256(n) => assert!((232..=255).contains(&n), "got {n}"),
731 other => panic!("expected Palette256, got {other:?}"),
732 }
733 }
734
735 #[test]
736 fn downgrade_truecolor_near_white_grayscale_saturates_without_overflow() {
737 for v in [239u8, 240, 248, 249, 255] {
741 let c = Color::TrueColor { r: v, g: v, b: v };
742 match c.downgrade(Capability::Palette256) {
743 Color::Palette256(n) => assert!(
744 (232..=255).contains(&n) || n == 231,
745 "r={v}: n={n} out of ramp/white range"
746 ),
747 other => panic!("expected Palette256, got {other:?}"),
748 }
749 }
750 }
751
752 #[test]
753 fn downgrade_truecolor_to_256_uses_cube_for_color() {
754 let red = Color::TrueColor { r: 255, g: 0, b: 0 };
756 assert_eq!(
757 red.downgrade(Capability::Palette256),
758 Color::Palette256(196)
759 );
760 let green = Color::TrueColor { r: 0, g: 255, b: 0 };
762 assert_eq!(
763 green.downgrade(Capability::Palette256),
764 Color::Palette256(46)
765 );
766 let blue = Color::TrueColor { r: 0, g: 0, b: 255 };
768 assert_eq!(
769 blue.downgrade(Capability::Palette256),
770 Color::Palette256(21)
771 );
772 }
773
774 #[test]
775 fn downgrade_palette256_cube_to_16_picks_a_color() {
776 match Color::Palette256(196).downgrade(Capability::Palette16) {
778 Color::Palette16(ac) => assert!(
779 matches!(ac, AnsiColor::Red | AnsiColor::BrightRed),
780 "got {ac:?}"
781 ),
782 other => panic!("expected Palette16, got {other:?}"),
783 }
784 }
785
786 #[test]
787 fn downgrade_palette256_low_16_passes_through_identity() {
788 assert_eq!(
790 Color::Palette256(1).downgrade(Capability::Palette16),
791 Color::Palette16(AnsiColor::Red),
792 );
793 assert_eq!(
794 Color::Palette256(15).downgrade(Capability::Palette16),
795 Color::Palette16(AnsiColor::BrightWhite),
796 );
797 }
798
799 #[test]
800 fn downgrade_palette256_grayscale_routes_to_blacks_or_whites() {
801 assert_eq!(
803 Color::Palette256(232).downgrade(Capability::Palette16),
804 Color::Palette16(AnsiColor::Black),
805 );
806 assert_eq!(
807 Color::Palette256(255).downgrade(Capability::Palette16),
808 Color::Palette16(AnsiColor::BrightWhite),
809 );
810 }
811
812 #[test]
813 fn downgrade_truecolor_red_to_16_picks_red_family() {
814 let red = Color::TrueColor {
815 r: 200,
816 g: 30,
817 b: 30,
818 };
819 match red.downgrade(Capability::Palette16) {
820 Color::Palette16(ac) => assert!(
821 matches!(ac, AnsiColor::Red | AnsiColor::BrightRed),
822 "got {ac:?}"
823 ),
824 other => panic!("expected Palette16, got {other:?}"),
825 }
826 }
827
828 #[test]
831 fn sgr_open_plain_style_emits_nothing() {
832 let s = Style::default();
833 let t = default_theme();
834 assert_eq!(sgr_open(&s, t, Capability::TrueColor), "");
835 }
836
837 #[test]
838 fn sgr_open_bold_only_emits_sgr1() {
839 let s = Style {
840 bold: true,
841 ..Style::default()
842 };
843 assert_eq!(sgr_open(&s, default_theme(), Capability::None), "\x1b[1m");
844 }
845
846 #[test]
847 fn sgr_open_role_under_palette16_emits_ansi_code() {
848 let s = Style::role(Role::Success);
849 let out = sgr_open(&s, default_theme(), Capability::Palette16);
850 assert_eq!(out, "\x1b[92m");
852 }
853
854 #[test]
855 fn sgr_open_role_under_no_capability_drops_color_keeps_decoration() {
856 let s = Style {
857 role: Some(Role::Error),
858 bold: true,
859 ..Style::default()
860 };
861 assert_eq!(sgr_open(&s, default_theme(), Capability::None), "\x1b[1m");
862 }
863
864 #[test]
865 fn sgr_open_explicit_fg_wins_over_role() {
866 let s = Style {
867 role: Some(Role::Warning),
868 fg: Some(Color::Palette16(AnsiColor::Blue)),
869 ..Style::default()
870 };
871 assert_eq!(
873 sgr_open(&s, default_theme(), Capability::Palette16),
874 "\x1b[34m"
875 );
876 }
877
878 #[test]
879 fn sgr_open_truecolor_fg_emits_38_2_sequence() {
880 let s = Style {
881 fg: Some(Color::TrueColor {
882 r: 12,
883 g: 34,
884 b: 56,
885 }),
886 ..Style::default()
887 };
888 assert_eq!(
889 sgr_open(&s, default_theme(), Capability::TrueColor),
890 "\x1b[38;2;12;34;56m"
891 );
892 }
893
894 #[test]
895 fn sgr_open_combines_decorations_and_color() {
896 let s = Style {
897 role: Some(Role::Primary),
898 bold: true,
899 italic: true,
900 ..Style::default()
901 };
902 assert_eq!(
904 sgr_open(&s, default_theme(), Capability::Palette16),
905 "\x1b[1;3;95m"
906 );
907 }
908
909 #[test]
910 fn sgr_open_dim_only_emits_sgr2() {
911 let s = Style {
912 dim: true,
913 ..Style::default()
914 };
915 assert_eq!(sgr_open(&s, default_theme(), Capability::None), "\x1b[2m");
916 }
917
918 #[test]
919 fn sgr_open_underline_only_emits_sgr4() {
920 let s = Style {
921 underline: true,
922 ..Style::default()
923 };
924 assert_eq!(sgr_open(&s, default_theme(), Capability::None), "\x1b[4m");
925 }
926
927 #[test]
928 fn sgr_open_full_decoration_stack_keeps_order() {
929 let s = Style {
930 role: Some(Role::Primary),
931 bold: true,
932 dim: true,
933 italic: true,
934 underline: true,
935 ..Style::default()
936 };
937 assert_eq!(
939 sgr_open(&s, default_theme(), Capability::Palette16),
940 "\x1b[1;2;3;4;95m"
941 );
942 }
943
944 #[test]
945 fn sgr_reset_is_stable_and_short() {
946 assert_eq!(sgr_reset(), "\x1b[0m");
947 }
948
949 #[test]
950 fn minimal_theme_emits_only_decorations() {
951 let s = Style {
952 role: Some(Role::Success),
953 bold: true,
954 ..Style::default()
955 };
956 let t = built_in("minimal").expect("minimal");
957 assert_eq!(sgr_open(&s, t, Capability::TrueColor), "\x1b[1m");
958 }
959
960 #[test]
963 fn from_env_vars_prefers_colorterm_truecolor() {
964 assert_eq!(
965 Capability::from_env_vars(Some("truecolor"), Some("xterm")),
966 Capability::TrueColor
967 );
968 assert_eq!(
969 Capability::from_env_vars(Some("24bit"), Some("xterm")),
970 Capability::TrueColor
971 );
972 assert_eq!(
973 Capability::from_env_vars(Some("TRUECOLOR"), None),
974 Capability::TrueColor
975 );
976 }
977
978 #[test]
979 fn from_env_vars_falls_back_to_term_256color() {
980 assert_eq!(
981 Capability::from_env_vars(None, Some("xterm-256color")),
982 Capability::Palette256
983 );
984 assert_eq!(
985 Capability::from_env_vars(None, Some("tmux-256color")),
986 Capability::Palette256
987 );
988 }
989
990 #[test]
991 fn from_env_vars_unknown_term_is_palette16() {
992 assert_eq!(
993 Capability::from_env_vars(None, Some("xterm")),
994 Capability::Palette16
995 );
996 assert_eq!(
997 Capability::from_env_vars(None, Some("vt100")),
998 Capability::Palette16
999 );
1000 }
1001
1002 #[test]
1003 fn from_env_vars_dumb_or_missing_term_is_none() {
1004 assert_eq!(
1005 Capability::from_env_vars(None, Some("dumb")),
1006 Capability::None
1007 );
1008 assert_eq!(
1010 Capability::from_env_vars(None, Some("DUMB")),
1011 Capability::None
1012 );
1013 assert_eq!(
1014 Capability::from_env_vars(None, Some(" dumb ")),
1015 Capability::None
1016 );
1017 assert_eq!(Capability::from_env_vars(None, Some("")), Capability::None);
1018 assert_eq!(Capability::from_env_vars(None, None), Capability::None);
1019 }
1020
1021 #[test]
1022 fn from_env_vars_colorterm_truecolor_overrides_term_dumb() {
1023 assert_eq!(
1025 Capability::from_env_vars(Some("truecolor"), Some("dumb")),
1026 Capability::TrueColor
1027 );
1028 }
1029
1030 #[test]
1033 fn force_from_picks_max_of_both_inputs() {
1034 assert_eq!(
1035 Capability::force_from(Capability::Palette256, Capability::TrueColor),
1036 Capability::TrueColor
1037 );
1038 assert_eq!(
1039 Capability::force_from(Capability::TrueColor, Capability::Palette256),
1040 Capability::TrueColor
1041 );
1042 assert_eq!(
1043 Capability::force_from(Capability::Palette16, Capability::Palette256),
1044 Capability::Palette256
1045 );
1046 }
1047
1048 #[test]
1049 fn force_from_truecolor_from_either_side_wins() {
1050 assert_eq!(
1051 Capability::force_from(Capability::None, Capability::TrueColor),
1052 Capability::TrueColor
1053 );
1054 assert_eq!(
1055 Capability::force_from(Capability::TrueColor, Capability::None),
1056 Capability::TrueColor
1057 );
1058 }
1059
1060 #[test]
1061 fn force_from_floors_at_palette16_when_both_inputs_are_none() {
1062 assert_eq!(
1065 Capability::force_from(Capability::None, Capability::None),
1066 Capability::Palette16
1067 );
1068 }
1069
1070 #[test]
1071 fn force_from_floor_overrides_dumb_term_when_env_probe_returns_none() {
1072 let env = Capability::from_env_vars(None, Some("dumb"));
1075 assert_eq!(env, Capability::None);
1076 assert_eq!(
1077 Capability::force_from(Capability::None, env),
1078 Capability::Palette16
1079 );
1080 }
1081
1082 #[test]
1083 fn from_env_vars_colorterm_garbage_falls_through_to_term() {
1084 assert_eq!(
1087 Capability::from_env_vars(Some("yes"), Some("xterm-256color")),
1088 Capability::Palette256
1089 );
1090 }
1091}