1use crate::color::{to_ansi16, to_ansi256};
4use crate::theme::Theme;
5use ratatui::style::Color;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub enum ColorSupport {
10 None,
12 Ansi16,
14 Ansi256,
16 TrueColor,
18}
19
20impl ColorSupport {
21 #[must_use]
23 pub fn detect() -> Self {
24 let get = |k: &str| std::env::var(k).ok();
25 Self::from_env(
26 get("NO_COLOR").as_deref(),
27 get("COLORTERM").as_deref(),
28 get("TERM").as_deref(),
29 )
30 }
31
32 #[must_use]
40 pub fn from_env(no_color: Option<&str>, colorterm: Option<&str>, term: Option<&str>) -> Self {
41 if no_color.is_some_and(|v| !v.is_empty()) {
42 return Self::None;
43 }
44 let colorterm = colorterm.unwrap_or("").to_ascii_lowercase();
45 let term = term.unwrap_or("").to_ascii_lowercase();
46 if colorterm.contains("truecolor") || colorterm.contains("24bit") || term.contains("direct")
47 {
48 Self::TrueColor
49 } else if term.contains("256color") {
50 Self::Ansi256
51 } else if term == "dumb" {
52 Self::None
53 } else {
54 Self::Ansi16
55 }
56 }
57
58 #[must_use]
60 pub fn downgrade_color(self, color: Color) -> Color {
61 match self {
62 Self::TrueColor => color,
63 Self::Ansi256 => to_ansi256(color),
64 Self::Ansi16 => to_ansi16(color),
65 Self::None => Color::Reset,
66 }
67 }
68}
69
70impl Theme {
71 #[must_use]
77 pub fn downgrade(&self, support: ColorSupport) -> Theme {
78 let mut t = self.clone();
79 t.map_colors(|c| support.downgrade_color(c));
80 if support == ColorSupport::None {
81 t.background = None;
82 t.surface = None;
83 }
84 t
85 }
86
87 #[must_use]
89 pub fn downgrade_for_terminal(&self) -> Theme {
90 self.downgrade(ColorSupport::detect())
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn no_color_wins() {
100 let s = ColorSupport::from_env(Some("1"), Some("truecolor"), Some("xterm-256color"));
101 assert_eq!(s, ColorSupport::None);
102 }
103
104 #[test]
105 fn empty_no_color_ignored() {
106 let s = ColorSupport::from_env(Some(""), Some("truecolor"), None);
107 assert_eq!(s, ColorSupport::TrueColor);
108 }
109
110 #[test]
111 fn colorterm_variants() {
112 assert_eq!(
113 ColorSupport::from_env(None, Some("24bit"), None),
114 ColorSupport::TrueColor
115 );
116 assert_eq!(
117 ColorSupport::from_env(None, Some("TrueColor"), Some("xterm")),
118 ColorSupport::TrueColor
119 );
120 }
121
122 #[test]
123 fn term_variants() {
124 let f = |t| ColorSupport::from_env(None, None, Some(t));
125 assert_eq!(f("xterm-256color"), ColorSupport::Ansi256);
126 assert_eq!(f("xterm-direct"), ColorSupport::TrueColor);
127 assert_eq!(f("dumb"), ColorSupport::None);
128 assert_eq!(f("xterm"), ColorSupport::Ansi16);
129 assert_eq!(
130 ColorSupport::from_env(None, None, None),
131 ColorSupport::Ansi16
132 );
133 }
134
135 #[test]
136 fn ordering_by_capability() {
137 assert!(ColorSupport::None < ColorSupport::Ansi16);
138 assert!(ColorSupport::Ansi256 < ColorSupport::TrueColor);
139 }
140
141 #[test]
142 fn downgrade_color_levels() {
143 let c = Color::Rgb(255, 0, 0);
144 assert_eq!(ColorSupport::TrueColor.downgrade_color(c), c);
145 assert_eq!(
146 ColorSupport::Ansi256.downgrade_color(c),
147 Color::Indexed(196)
148 );
149 assert_eq!(ColorSupport::Ansi16.downgrade_color(c), Color::LightRed);
150 assert_eq!(ColorSupport::None.downgrade_color(c), Color::Reset);
151 }
152
153 #[test]
154 fn truecolor_downgrade_is_identity() {
155 let t = Theme::dracula();
156 assert_eq!(t.downgrade(ColorSupport::TrueColor), t);
157 }
158
159 #[test]
160 fn none_clears_backgrounds() {
161 let t = Theme::dracula().downgrade(ColorSupport::None);
162 assert_eq!(t.background, None);
163 assert_eq!(t.surface, None);
164 assert_eq!(t.foreground, Color::Reset);
165 }
166}