1#![allow(clippy::bool_to_int_with_if)]
25
26use std::env;
27use std::sync::OnceLock;
28
29#[derive(Clone, Copy, Debug)]
31pub enum Stream {
32 Stdout,
33 Stderr,
34}
35
36fn env_force_color() -> usize {
37 if let Ok(force) = env::var("FORCE_COLOR") {
38 match force.as_ref() {
39 "true" | "" => 1,
40 "false" => 0,
41 f => std::cmp::min(f.parse().unwrap_or(1), 3),
42 }
43 } else if let Ok(cli_clr_force) = env::var("CLICOLOR_FORCE") {
44 if cli_clr_force != "0" {
45 1
46 } else {
47 0
48 }
49 } else {
50 0
51 }
52}
53
54fn env_no_color() -> bool {
55 match as_str(&env::var("NO_COLOR")) {
56 Ok("0") | Err(_) => false,
57 Ok(_) => true,
58 }
59}
60
61fn as_str<E>(option: &Result<String, E>) -> Result<&str, &E> {
63 match option {
64 Ok(inner) => Ok(inner),
65 Err(e) => Err(e),
66 }
67}
68
69fn translate_level(level: usize) -> Option<ColorLevel> {
70 if level == 0 {
71 None
72 } else {
73 Some(ColorLevel {
74 level,
75 has_basic: true,
76 has_256: level >= 2,
77 has_16m: level >= 3,
78 })
79 }
80}
81
82fn is_a_tty(stream: Stream) -> bool {
83 use std::io::IsTerminal;
84 match stream {
85 Stream::Stdout => std::io::stdout().is_terminal(),
86 Stream::Stderr => std::io::stderr().is_terminal(),
87 }
88}
89
90fn supports_color(stream: Stream) -> usize {
91 let force_color = env_force_color();
92 if force_color > 0 {
93 force_color
94 } else if env_no_color()
95 || as_str(&env::var("TERM")) == Ok("dumb")
96 || !(is_a_tty(stream) || env::var("IGNORE_IS_TERMINAL").map_or(false, |v| v != "0"))
97 {
98 0
99 } else if env::var("COLORTERM").map(|colorterm| check_colorterm_16m(&colorterm)) == Ok(true)
100 || env::var("TERM").map(|term| check_term_16m(&term)) == Ok(true)
101 || as_str(&env::var("TERM_PROGRAM")) == Ok("iTerm.app")
102 {
103 3
104 } else if as_str(&env::var("TERM_PROGRAM")) == Ok("Apple_Terminal")
105 || env::var("TERM").map(|term| check_256_color(&term)) == Ok(true)
106 {
107 2
108 } else if env::var("COLORTERM").is_ok()
109 || check_ansi_color(env::var("TERM").ok().as_deref())
110 || env::var("CLICOLOR").map_or(false, |v| v != "0")
111 || is_ci::uncached()
112 {
113 1
114 } else {
115 0
116 }
117}
118
119#[cfg(windows)]
120fn check_ansi_color(term: Option<&str>) -> bool {
121 if let Some(term) = term {
122 term != "dumb" && term != "cygwin"
124 } else {
125 true
128 }
129}
130
131#[cfg(not(windows))]
132fn check_ansi_color(term: Option<&str>) -> bool {
133 if let Some(term) = term {
134 term != "dumb"
136 } else {
137 false
139 }
140}
141
142fn check_colorterm_16m(colorterm: &str) -> bool {
143 colorterm == "truecolor" || colorterm == "24bit"
144}
145
146fn check_term_16m(term: &str) -> bool {
147 term.ends_with("direct") || term.ends_with("truecolor")
148}
149
150fn check_256_color(term: &str) -> bool {
151 term.ends_with("256") || term.ends_with("256color")
152}
153
154pub fn on(stream: Stream) -> Option<ColorLevel> {
158 translate_level(supports_color(stream))
159}
160
161macro_rules! assert_stream_in_bounds {
162 ($($variant:ident)*) => {
163 $(
164 const _: () = [(); 2][Stream::$variant as usize];
165 )*
166 };
167}
168
169assert_stream_in_bounds!(Stdout Stderr);
171
172pub fn on_cached(stream: Stream) -> Option<ColorLevel> {
179 static CACHE: [OnceLock<Option<ColorLevel>>; 2] = [OnceLock::new(), OnceLock::new()];
180
181 let stream_index = stream as usize;
182 *CACHE[stream_index].get_or_init(|| translate_level(supports_color(stream)))
183}
184
185#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
191pub struct ColorLevel {
192 level: usize,
193 pub has_basic: bool,
195 pub has_256: bool,
197 pub has_16m: bool,
199}
200
201#[cfg(test)]
202mod tests {
203 use std::sync::Mutex;
204
205 use super::*;
206
207 static TEST_LOCK: Mutex<()> = Mutex::new(());
209
210 fn set_up() {
211 env::vars().for_each(|(k, _v)| env::remove_var(k));
213 }
214
215 #[test]
216 #[cfg_attr(miri, ignore)]
217 fn test_empty_env() {
218 let _test_guard = TEST_LOCK.lock().unwrap();
219 set_up();
220
221 assert_eq!(on(Stream::Stdout), None);
222 }
223
224 #[test]
225 #[cfg_attr(miri, ignore)]
226 fn test_clicolor_ansi() {
227 let _test_guard = TEST_LOCK.lock().unwrap();
228 set_up();
229
230 env::set_var("IGNORE_IS_TERMINAL", "1");
231 env::set_var("CLICOLOR", "1");
232 let expected = Some(ColorLevel {
233 level: 1,
234 has_basic: true,
235 has_256: false,
236 has_16m: false,
237 });
238 assert_eq!(on(Stream::Stdout), expected);
239
240 env::set_var("CLICOLOR", "0");
241 assert_eq!(on(Stream::Stdout), None);
242 }
243
244 #[test]
245 #[cfg_attr(miri, ignore)]
246 fn test_on_cached() {
247 let _test_guard = TEST_LOCK.lock().unwrap();
248 set_up();
249 env::set_var("IGNORE_IS_TERMINAL", "1");
250
251 env::set_var("CLICOLOR", "1");
252 assert!(on(Stream::Stdout).is_some());
253 assert!(on_cached(Stream::Stdout).is_some());
254
255 env::set_var("CLICOLOR", "0");
256 assert!(on(Stream::Stdout).is_none());
257 assert!(on_cached(Stream::Stdout).is_some());
258 }
259
260 #[test]
261 #[cfg_attr(miri, ignore)]
262 fn test_clicolor_force_ansi() {
263 let _test_guard = TEST_LOCK.lock().unwrap();
264 set_up();
265
266 env::set_var("CLICOLOR", "0");
267 env::set_var("CLICOLOR_FORCE", "1");
268 let expected = Some(ColorLevel {
269 level: 1,
270 has_basic: true,
271 has_256: false,
272 has_16m: false,
273 });
274 assert_eq!(on(Stream::Stdout), expected);
275 }
276}