1#[cfg(doc)]
19use crate::StyledValue;
20
21use core::{str::FromStr, sync::atomic::AtomicU8};
22
23static COLORING_MODE: AtomicU8 = AtomicU8::new(Mode::DETECT);
24static DEFAULT_STREAM: AtomicU8 = AtomicU8::new(Stream::AlwaysColor.encode());
25#[cfg(any(feature = "std", feature = "supports-color"))]
26static STDOUT_SUPPORT: AtomicU8 = AtomicU8::new(ColorSupport::DETECT);
27#[cfg(any(feature = "std", feature = "supports-color"))]
28static STDERR_SUPPORT: AtomicU8 = AtomicU8::new(ColorSupport::DETECT);
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Mode {
33 Detect,
35 Always,
37 Never,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct ModeFromStrError;
44
45#[cfg(feature = "std")]
46impl std::error::Error for ModeFromStrError {}
47
48impl core::fmt::Display for ModeFromStrError {
49 #[inline]
50 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51 f.write_str(r#"Invalid mode: valid options include "detect", "always", "never""#)
52 }
53}
54
55const ASCII_CASE_MASK: u8 = 0b0010_0000;
56const ASCII_CASE_MASK_SIMD: u64 = u64::from_ne_bytes([ASCII_CASE_MASK; 8]);
57
58impl FromStr for Mode {
59 type Err = ModeFromStrError;
60
61 #[inline]
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 Self::from_ascii_bytes(s.as_bytes())
64 }
65}
66
67impl Mode {
68 #[inline]
70 pub const fn from_ascii_bytes(s: &[u8]) -> Result<Self, ModeFromStrError> {
71 const DETECT_STR: u64 = u64::from_ne_bytes(*b"detect\0\0") | ASCII_CASE_MASK_SIMD;
72 const ALWAYS_STR: u64 = u64::from_ne_bytes(*b"always\0\0") | ASCII_CASE_MASK_SIMD;
73 const NEVER_STR: u64 = u64::from_ne_bytes(*b"never\0\0\0") | ASCII_CASE_MASK_SIMD;
74
75 let data = match *s {
76 [a, b, c, d, e] => u64::from_ne_bytes([a, b, c, d, e, 0, 0, 0]),
77 [a, b, c, d, e, f] => u64::from_ne_bytes([a, b, c, d, e, f, 0, 0]),
78 _ => return Err(ModeFromStrError),
79 };
80
81 let data = data | ASCII_CASE_MASK_SIMD;
82
83 match data {
84 DETECT_STR => Ok(Mode::Detect),
85 ALWAYS_STR => Ok(Mode::Always),
86 NEVER_STR => Ok(Mode::Never),
87 _ => Err(ModeFromStrError),
88 }
89 }
90}
91
92#[non_exhaustive]
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum Stream {
96 Stdout,
98 Stderr,
100 AlwaysColor,
104 NeverColor,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct StreamFromStrError;
111
112#[cfg(feature = "std")]
113impl std::error::Error for StreamFromStrError {}
114
115impl core::fmt::Display for StreamFromStrError {
116 #[inline]
117 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118 f.write_str(r#"Invalid mode: valid options include "stdout", "stderr", "always", "never""#)
119 }
120}
121
122impl FromStr for Stream {
123 type Err = StreamFromStrError;
124
125 #[inline]
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 Self::from_ascii_bytes(s.as_bytes())
128 }
129}
130
131impl Stream {
132 #[inline]
134 pub const fn from_ascii_bytes(s: &[u8]) -> Result<Self, StreamFromStrError> {
135 const STDOUT_STR: u64 = u64::from_ne_bytes(*b"stdout\0\0") | ASCII_CASE_MASK_SIMD;
136 const STDERR_STR: u64 = u64::from_ne_bytes(*b"stderr\0\0") | ASCII_CASE_MASK_SIMD;
137 const ALWAYS_STR: u64 = u64::from_ne_bytes(*b"always\0\0") | ASCII_CASE_MASK_SIMD;
138 const NEVER_STR: u64 = u64::from_ne_bytes(*b"never\0\0\0") | ASCII_CASE_MASK_SIMD;
139
140 let data = match *s {
141 [a, b, c, d, e] => u64::from_ne_bytes([a, b, c, d, e, 0, 0, 0]),
142 [a, b, c, d, e, f] => u64::from_ne_bytes([a, b, c, d, e, f, 0, 0]),
143 _ => return Err(StreamFromStrError),
144 };
145
146 let data = data | ASCII_CASE_MASK_SIMD;
147
148 match data {
149 STDERR_STR => Ok(Stream::Stderr),
150 STDOUT_STR => Ok(Stream::Stdout),
151 ALWAYS_STR => Ok(Stream::AlwaysColor),
152 NEVER_STR => Ok(Stream::NeverColor),
153 _ => Err(StreamFromStrError),
154 }
155 }
156}
157
158#[repr(u8)]
160#[non_exhaustive]
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162pub enum ColorKind {
163 Ansi,
165 Xterm,
167 Rgb,
169 NoColor,
171}
172
173#[cfg(any(feature = "std", feature = "supports-color"))]
174#[non_exhaustive]
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176struct ColorSupport {
177 ansi: bool,
178 xterm: bool,
179 rgb: bool,
180}
181
182#[cfg(any(feature = "std", feature = "supports-color"))]
183impl ColorSupport {
184 const DETECT: u8 = 0x80;
185
186 #[cfg(feature = "supports-color")]
187 fn encode(self) -> u8 {
188 u8::from(self.ansi) | u8::from(self.xterm) << 1 | u8::from(self.rgb) << 2
189 }
190
191 #[cfg(feature = "supports-color")]
192 fn decode(x: u8) -> Self {
193 Self {
194 ansi: x & 0b001 != 0,
195 xterm: x & 0b010 != 0,
196 rgb: x & 0b100 != 0,
197 }
198 }
199}
200
201impl Mode {
202 const DETECT: u8 = Self::Detect.encode();
203
204 const fn encode(self) -> u8 {
205 match self {
206 Mode::Always => 0,
207 Mode::Never => 1,
208 Mode::Detect => 2,
209 }
210 }
211
212 const fn decode(x: u8) -> Self {
213 match x {
214 0 => Self::Always,
215 1 => Self::Never,
216 _ => Self::Detect,
217 }
218 }
219
220 #[cfg(feature = "std")]
228 #[cfg_attr(doc, doc(cfg(feature = "std")))]
229 pub fn from_env() -> Option<Self> {
230 if std::env::var_os("NO_COLOR").is_some_and(|x| x != "0") {
231 return Some(Self::Never);
232 }
233
234 if std::env::var_os("ALWAYS_COLOR").is_some_and(|x| x != "0") {
235 return Some(Self::Always);
236 }
237
238 if std::env::var_os("CLICOLOR_FORCE").is_some_and(|x| x != "0") {
239 return Some(Self::Always);
240 }
241
242 if std::env::var_os("FORCE_COLOR").is_some_and(|x| x != "0") {
243 return Some(Self::Always);
244 }
245
246 None
247 }
248}
249
250impl Stream {
251 const fn encode(self) -> u8 {
252 match self {
253 Stream::Stdout => 0,
254 Stream::Stderr => 1,
255 Stream::AlwaysColor => 2,
256 Stream::NeverColor => 3,
257 }
258 }
259
260 const fn decode(x: u8) -> Self {
261 match x {
262 0 => Self::Stdout,
263 1 => Self::Stderr,
264 2 => Self::AlwaysColor,
265 3 => Self::NeverColor,
266 _ => unreachable!(),
267 }
268 }
269}
270
271#[inline]
272pub fn set_coloring_mode(mode: Mode) {
274 if cfg!(feature = "strip-colors") {
275 return;
276 }
277
278 COLORING_MODE.store(Mode::encode(mode), core::sync::atomic::Ordering::Release)
279}
280
281#[inline]
287#[cfg(feature = "std")]
288#[cfg_attr(doc, doc(cfg(feature = "std")))]
289pub fn set_coloring_mode_from_env() {
290 if cfg!(feature = "strip-colors") {
291 return;
292 }
293
294 if let Some(mode) = Mode::from_env() {
295 set_coloring_mode(mode)
296 }
297}
298
299#[inline]
306pub fn get_coloring_mode() -> Mode {
307 if cfg!(feature = "strip-colors") {
308 return Mode::Never;
309 }
310
311 Mode::decode(COLORING_MODE.load(core::sync::atomic::Ordering::Acquire))
312}
313
314#[inline]
323pub fn set_default_stream(stream: Stream) {
324 DEFAULT_STREAM.store(
325 Stream::encode(stream),
326 core::sync::atomic::Ordering::Release,
327 )
328}
329
330#[inline]
335pub fn get_default_stream() -> Stream {
336 Stream::decode(DEFAULT_STREAM.load(core::sync::atomic::Ordering::Acquire))
337}
338
339#[inline]
402pub fn should_color(stream: Option<Stream>, kinds: &[ColorKind]) -> bool {
403 if cfg!(feature = "strip-colors") {
404 return false;
405 }
406
407 match get_coloring_mode() {
408 Mode::Always => return true,
409 Mode::Never => return false,
410 Mode::Detect => (),
411 }
412
413 let stream = stream.unwrap_or_else(get_default_stream);
414
415 let is_stdout = match stream {
416 Stream::Stdout => true,
417 Stream::Stderr => false,
418 Stream::AlwaysColor => return true,
419 Stream::NeverColor => return false,
420 };
421
422 should_color_slow(is_stdout, kinds)
423}
424
425#[inline]
426#[allow(clippy::missing_const_for_fn)]
427#[cfg(all(not(feature = "std"), not(feature = "supports-color")))]
428fn should_color_slow(_is_stdout: bool, _kinds: &[ColorKind]) -> bool {
429 true
430}
431
432#[cold]
433#[cfg(all(feature = "std", not(feature = "supports-color")))]
434fn should_color_slow(is_stdout: bool, _kinds: &[ColorKind]) -> bool {
435 use core::sync::atomic::Ordering;
436 use std::io::IsTerminal;
437
438 let support_ref = match is_stdout {
439 true => &STDOUT_SUPPORT,
440 false => &STDERR_SUPPORT,
441 };
442
443 #[cold]
444 #[inline(never)]
445 fn detect(is_stdout: bool, support: &AtomicU8) -> bool {
446 let s = if is_stdout {
447 std::io::stdout().is_terminal()
448 } else {
449 std::io::stderr().is_terminal()
450 };
451
452 support.store(s as u8, Ordering::Relaxed);
453
454 core::sync::atomic::fence(Ordering::SeqCst);
455
456 s
457 }
458
459 match support_ref.load(Ordering::Acquire) {
460 ColorSupport::DETECT => detect(is_stdout, support_ref),
461 0 => false,
462 _ => true,
463 }
464}
465
466#[cold]
467#[cfg(feature = "supports-color")]
468fn should_color_slow(is_stdout: bool, kinds: &[ColorKind]) -> bool {
469 use core::sync::atomic::Ordering;
470
471 use supports_color::Stream;
472
473 let (stream, support_ref) = match is_stdout {
474 true => (Stream::Stdout, &STDOUT_SUPPORT),
475 false => (Stream::Stderr, &STDERR_SUPPORT),
476 };
477
478 let support = support_ref.load(Ordering::Acquire);
479
480 #[cold]
481 #[inline(never)]
482 fn detect(s: Stream, support: &AtomicU8) -> ColorSupport {
483 let s = supports_color::on(s).map_or(
484 ColorSupport {
485 ansi: false,
486 xterm: false,
487 rgb: false,
488 },
489 |level| ColorSupport {
490 ansi: level.has_basic,
491 xterm: level.has_256,
492 rgb: level.has_16m,
493 },
494 );
495
496 support.store(s.encode(), Ordering::Relaxed);
497
498 core::sync::atomic::fence(Ordering::SeqCst);
499
500 s
501 }
502
503 let support = if support == ColorSupport::DETECT {
504 detect(stream, support_ref)
505 } else {
506 ColorSupport::decode(support)
507 };
508
509 for &kind in kinds {
510 let supported = match kind {
511 ColorKind::Ansi => support.ansi,
512 ColorKind::Xterm => support.xterm,
513 ColorKind::Rgb => support.rgb,
514 ColorKind::NoColor => continue,
515 };
516
517 if !supported {
518 return false;
519 }
520 }
521
522 true
523}
524
525#[cfg(test)]
526mod test {
527 use crate::mode::Mode;
528
529 use super::Stream;
530
531 extern crate std;
532
533 #[allow(clippy::needless_range_loop)]
534 fn test_case_insensitive_mode_from_str<const N: usize>(input: [u8; N], mode: Mode) {
535 for i in 0..1 << N {
536 let mut input = input;
537 for j in 0..input.len() {
538 if i & (1 << j) != 0 {
539 input[j] = input[j].to_ascii_uppercase();
540 };
541 }
542
543 assert_eq!(Mode::from_ascii_bytes(&input), Ok(mode));
544 }
545 }
546
547 #[allow(clippy::needless_range_loop)]
548 fn test_case_insensitive_stream_from_str<const N: usize>(input: [u8; N], stream: Stream) {
549 for i in 0..1 << N {
550 let mut input = input;
551 for j in 0..input.len() {
552 if i & (1 << j) != 0 {
553 input[j] = input[j].to_ascii_uppercase();
554 };
555 }
556
557 assert_eq!(Stream::from_ascii_bytes(&input), Ok(stream));
558 }
559 }
560
561 #[test]
562 fn mode_from_str_never() {
563 test_case_insensitive_mode_from_str(*b"never", Mode::Never);
564 }
565
566 #[test]
567 fn mode_from_str_always() {
568 test_case_insensitive_mode_from_str(*b"always", Mode::Always);
569 }
570
571 #[test]
572 fn mode_from_str_detect() {
573 test_case_insensitive_mode_from_str(*b"detect", Mode::Detect);
574 }
575
576 #[test]
577 fn stream_from_str_never() {
578 test_case_insensitive_stream_from_str(*b"never", Stream::NeverColor);
579 }
580
581 #[test]
582 fn stream_from_str_always() {
583 test_case_insensitive_stream_from_str(*b"always", Stream::AlwaysColor);
584 }
585
586 #[test]
587 fn stream_from_str_stdout() {
588 test_case_insensitive_stream_from_str(*b"stdout", Stream::Stdout);
589 }
590
591 #[test]
592 fn stream_from_str_stderr() {
593 test_case_insensitive_stream_from_str(*b"stderr", Stream::Stderr);
594 }
595}