1#![allow(clippy::bool_to_int_with_if)]
25
26pub use atty::Stream;
27
28use std::cell::UnsafeCell;
29use std::env;
30use std::sync::Once;
31
32fn env_force_color() -> usize {
33 if let Ok(force) = env::var("FORCE_COLOR") {
34 match force.as_ref() {
35 "true" | "" => 1,
36 "false" => 0,
37 f => std::cmp::min(f.parse().unwrap_or(1), 3),
38 }
39 } else if let Ok(cli_clr_force) = env::var("CLICOLOR_FORCE") {
40 if cli_clr_force != "0" {
41 1
42 } else {
43 0
44 }
45 } else {
46 0
47 }
48}
49
50fn env_no_color() -> bool {
51 match as_str(&env::var("NO_COLOR")) {
52 Ok("0") | Err(_) => false,
53 Ok(_) => true,
54 }
55}
56
57fn as_str<E>(option: &Result<String, E>) -> Result<&str, &E> {
59 match option {
60 Ok(inner) => Ok(inner),
61 Err(e) => Err(e),
62 }
63}
64
65fn translate_level(level: usize) -> Option<ColorLevel> {
66 if level == 0 {
67 None
68 } else {
69 Some(ColorLevel {
70 level,
71 has_basic: true,
72 has_256: level >= 2,
73 has_16m: level >= 3,
74 })
75 }
76}
77
78fn supports_color(stream: Stream) -> usize {
79 let force_color = env_force_color();
80 if force_color > 0 {
81 force_color
82 } else if env_no_color() || !atty::is(stream) || as_str(&env::var("TERM")) == Ok("dumb") {
83 0
84 } else if as_str(&env::var("COLORTERM")) == Ok("truecolor")
85 || as_str(&env::var("TERM_PROGRAM")) == Ok("iTerm.app")
86 {
87 3
88 } else if as_str(&env::var("TERM_PROGRAM")) == Ok("Apple_Terminal")
89 || env::var("TERM").map(|term| check_256_color(&term)) == Ok(true)
90 {
91 2
92 } else if env::var("COLORTERM").is_ok()
93 || env::var("TERM").map(|term| check_ansi_color(&term)) == Ok(true)
94 || env::consts::OS == "windows"
95 || env::var("CLICOLOR").map_or(false, |v| v != "0")
96 || is_ci::uncached()
97 {
98 1
99 } else {
100 0
101 }
102}
103
104fn check_ansi_color(term: &str) -> bool {
105 term.starts_with("screen")
106 || term.starts_with("xterm")
107 || term.starts_with("vt100")
108 || term.starts_with("vt220")
109 || term.starts_with("rxvt")
110 || term.contains("color")
111 || term.contains("ansi")
112 || term.contains("cygwin")
113 || term.contains("linux")
114}
115
116fn check_256_color(term: &str) -> bool {
117 term.ends_with("256") || term.ends_with("256color")
118}
119
120pub fn on(stream: Stream) -> Option<ColorLevel> {
124 translate_level(supports_color(stream))
125}
126
127struct CacheCell(UnsafeCell<Option<ColorLevel>>);
128
129unsafe impl Sync for CacheCell {}
130
131static INIT: [Once; 3] = [Once::new(), Once::new(), Once::new()];
132static ON_CACHE: [CacheCell; 3] = [
133 CacheCell(UnsafeCell::new(None)),
134 CacheCell(UnsafeCell::new(None)),
135 CacheCell(UnsafeCell::new(None)),
136];
137
138macro_rules! assert_stream_in_bounds {
139 ($($variant:ident)*) => {
140 $(
141 const _: () = [(); 3][Stream::$variant as usize];
142 )*
143 };
144}
145
146assert_stream_in_bounds!(Stdout Stderr Stdin);
148
149pub fn on_cached(stream: Stream) -> Option<ColorLevel> {
156 let stream_index = stream as usize;
157 INIT[stream_index].call_once(|| unsafe {
158 *ON_CACHE[stream_index].0.get() = translate_level(supports_color(stream));
159 });
160
161 unsafe { *ON_CACHE[stream_index].0.get() }
162}
163
164#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
170pub struct ColorLevel {
171 level: usize,
172 pub has_basic: bool,
174 pub has_256: bool,
176 pub has_16m: bool,
178}
179
180#[cfg(test)]
181mod tests {
182 use std::sync::Mutex;
183
184 use super::*;
185
186 static TEST_LOCK: Mutex<()> = Mutex::new(());
188
189 fn set_up() {
190 env::vars().for_each(|(k, _v)| env::remove_var(k));
192 }
193
194 #[test]
195 #[cfg_attr(miri, ignore)]
196 fn test_empty_env() {
197 let _test_guard = TEST_LOCK.lock().unwrap();
198 set_up();
199
200 assert_eq!(on(atty::Stream::Stdout), None);
201 }
202
203 #[test]
204 #[cfg_attr(miri, ignore)]
205 fn test_clicolor_ansi() {
206 let _test_guard = TEST_LOCK.lock().unwrap();
207 set_up();
208
209 env::set_var("CLICOLOR", "1");
210 let expected = Some(ColorLevel {
211 level: 1,
212 has_basic: true,
213 has_256: false,
214 has_16m: false,
215 });
216 assert_eq!(on(atty::Stream::Stdout), expected);
217
218 env::set_var("CLICOLOR", "0");
219 assert_eq!(on(atty::Stream::Stdout), None);
220 }
221
222 #[test]
223 #[cfg_attr(miri, ignore)]
224 fn test_clicolor_force_ansi() {
225 let _test_guard = TEST_LOCK.lock().unwrap();
226 set_up();
227
228 env::set_var("CLICOLOR", "0");
229 env::set_var("CLICOLOR_FORCE", "1");
230 let expected = Some(ColorLevel {
231 level: 1,
232 has_basic: true,
233 has_256: false,
234 has_16m: false,
235 });
236 assert_eq!(on(atty::Stream::Stdout), expected);
237 }
238}