1use crate::error::ColorError;
8use std::env;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
12pub enum ColorSupport {
13 NoColor = 0,
14 Basic = 1, Color256 = 2, TrueColor = 3, }
18
19impl ColorSupport {
20 pub fn supports(&self, required: ColorSupport) -> bool {
22 *self >= required
23 }
24}
25
26impl std::fmt::Display for ColorSupport {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 ColorSupport::NoColor => write!(f, "No Color"),
30 ColorSupport::Basic => write!(f, "Basic"),
31 ColorSupport::Color256 => write!(f, "Color256"),
32 ColorSupport::TrueColor => write!(f, "TrueColor"),
33 }
34 }
35}
36
37pub fn check_color_support() -> Result<ColorSupport, ColorError> {
58 if env::var("NO_COLOR").is_ok() {
60 return Ok(ColorSupport::NoColor);
61 }
62
63 let clicolor = env::var("CLICOLOR").unwrap_or_default();
64 if clicolor == "0" {
65 return Ok(ColorSupport::NoColor);
66 }
67
68 let mut support = ColorSupport::NoColor;
69
70 let colorterm = env::var("COLORTERM").unwrap_or_default();
72 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
73 support = ColorSupport::TrueColor;
74 }
75
76 let term = env::var("TERM").unwrap_or_default().to_lowercase();
77
78 let truecolor_terms = [
80 "xterm-truecolor",
81 "konsole",
82 "tmux",
83 "screen-truecolor",
84 "alacritty",
85 "kitty",
86 "terminator",
87 "terminology",
88 "eterm",
89 "rxvt-unicode",
90 "xterm-ghostty",
91 "vte",
92 "termious",
93 ];
94 if truecolor_terms.iter().any(|&t| term.contains(t)) {
95 support = ColorSupport::TrueColor;
96 }
97
98 let term_program = env::var("TERM_PROGRAM").unwrap_or_default();
100 if ["iTerm.app", "Apple_Terminal", "Hyper"].contains(&term_program.as_str()) {
101 support = ColorSupport::TrueColor;
102 }
103
104 if support == ColorSupport::NoColor {
106 if term.contains("256color") || term.contains("256") {
107 support = ColorSupport::Color256;
108 } else if term.contains("color")
109 || term.contains("ansi")
110 || term.contains("xterm")
111 || term.contains("screen")
112 {
113 support = ColorSupport::Basic;
114 }
115 }
116
117 let clicolor_force = env::var("CLICOLOR_FORCE").unwrap_or_default();
119 if clicolor_force == "1" {
120 if support == ColorSupport::NoColor {
123 support = ColorSupport::Basic;
124 }
125 }
126
127 Ok(support)
128}
129
130pub fn is_color_available() -> Result<(), ColorError> {
136 match check_color_support()? {
137 ColorSupport::NoColor => Err(ColorError::NoTerminalSupport),
138 _ => Ok(()),
139 }
140}
141
142#[cfg(test)]
143pub(crate) mod tests {
144 use super::*;
145 use crate::Color;
146
147 pub(crate) fn run_with_env_vars<F, T>(vars: &[(&str, Option<&str>)], test: F) -> T
148 where
149 F: FnOnce() -> T,
150 {
151 let original: Vec<(String, Option<String>)> = vars
153 .iter()
154 .map(|(k, _)| (k.to_string(), env::var(k).ok()))
155 .collect();
156
157 let is_ci = env::var("CI").is_ok()
159 || env::var("GITHUB_ACTIONS").is_ok()
160 || env::var("GITLAB_CI").is_ok()
161 || env::var("TRAVIS").is_ok()
162 || env::var("CIRCLECI").is_ok();
163
164 if is_ci {
166 env::set_var("COLORTERM", "truecolor");
167 env::set_var("TERM", "xterm-256color");
168 env::remove_var("NO_COLOR");
169 }
170
171 for (key, value) in vars {
173 match value {
174 Some(v) => env::set_var(key, v),
175 None => env::remove_var(key),
176 }
177 }
178
179 let result = test();
181
182 for (key, value) in original {
184 match value {
185 Some(v) => env::set_var(&key, v),
186 None => env::remove_var(&key),
187 }
188 }
189
190 result
191 }
192
193 #[test]
194 fn test_all_color_scenarios() {
195 run_with_env_vars(
197 &[
198 ("NO_COLOR", None),
199 ("COLORTERM", Some("truecolor")),
200 ("TERM", Some("xterm-256color")),
201 ],
202 || {
203 let support = check_color_support();
204 assert!(support.is_ok());
205 },
206 );
207
208 run_with_env_vars(
210 &[("NO_COLOR", Some("")), ("TERM", None), ("COLORTERM", None)],
211 || {
212 let support = check_color_support().expect("Color support check failed");
213 assert_eq!(support, ColorSupport::NoColor);
214 },
215 );
216
217 run_with_env_vars(
219 &[
220 ("NO_COLOR", None),
221 ("COLORTERM", None),
222 ("TERM", None),
223 ("CLICOLOR_FORCE", Some("1")),
224 ("CLICOLOR", None),
225 ],
226 || {
227 let support = check_color_support().expect("Color support check failed");
228 assert_eq!(support, ColorSupport::Basic);
229 },
230 );
231
232 run_with_env_vars(
234 &[
235 ("CLICOLOR", Some("0")),
236 ("NO_COLOR", None),
237 ("COLORTERM", None),
238 ("TERM", None),
239 ("CLICOLOR_FORCE", None),
240 ],
241 || {
242 let support = check_color_support().expect("Color support check failed");
243 assert_eq!(support, ColorSupport::NoColor);
244 },
245 );
246
247 run_with_env_vars(
249 &[
250 ("NO_COLOR", None),
251 ("COLORTERM", Some("truecolor")),
252 ("TERM", Some("xterm-256color")),
253 ],
254 || {
255 let rgb = Color::new_rgb(255, 128, 0);
256 assert!(rgb.is_ok());
257 },
258 );
259
260 run_with_env_vars(
262 &[
263 ("NO_COLOR", None),
264 ("COLORTERM", Some("truecolor")),
265 ("TERM", Some("xterm-256color")),
266 ],
267 || {
268 let hex = Color::new_hex("#FF8000");
269 assert!(hex.is_ok());
270 },
271 );
272 }
273}