colorz/
mode.rs

1//! Flags to control if any styling should occur
2//!
3//! There are three levels, in order of precedence
4//! * feature flags - compile time (`strip-colors`)
5//! * global - runtime [`set_coloring_mode`], [`set_coloring_mode_from_env`]
6//! * per value - runtime [`StyledValue::stream`]
7//!
8//! higher precedence options forces coloring or no-coloring even if lower precedence options
9//! specify otherwise.
10//!
11//! For example, using [`StyledValue::stream`] to [`Stream::AlwaysColor`] doesn't guarantee
12//! that any coloring will happen. For example, if the `strip-colors` feature flag is set
13//! or if `set(Mode::Never)` was called before.
14//!
15//! However, these flags only control coloring on [`StyledValue`], so using
16//! the color types directly to color values will always be supported (even with `strip-colors`).
17
18#[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/// The coloring mode
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Mode {
33    /// use [`StyledValue::stream`] to pick when to color (by default always color if stream isn't specified)
34    Detect,
35    /// Always color [`StyledValue`]
36    Always,
37    /// Never color [`StyledValue`]
38    Never,
39}
40
41/// An error if deserializing a mode from a string fails
42#[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    /// Parse the mode from some ascii encoded bytes
69    #[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/// The stream to detect when to color on
93#[non_exhaustive]
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum Stream {
96    /// Detect via [`std::io::stdout`] if feature `std` or `supports-color` is enabled
97    Stdout,
98    /// Detect via [`std::io::stderr`] if feature `std` or `supports-color` is enabled
99    Stderr,
100    /// Always color, used to pick the coloring mode at runtime for a particular value
101    ///
102    /// The default coloring mode for streams
103    AlwaysColor,
104    /// Never color, used to pick the coloring mode at runtime for a particular value
105    NeverColor,
106}
107
108/// An error if deserializing a mode from a string fails
109#[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    /// Parse the mode from some ascii encoded bytes
133    #[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/// The coloring kinds
159#[repr(u8)]
160#[non_exhaustive]
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162pub enum ColorKind {
163    /// A basic ANSI color
164    Ansi,
165    /// A 256-color
166    Xterm,
167    /// A 48-bit color
168    Rgb,
169    /// No color at all
170    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    /// Reads the current mode from the environment
221    ///
222    /// * If `NO_COLOR` is set to a non-zero value, [`Mode::Never`] is returned
223    ///
224    /// * If `ALWAYS_COLOR`, `CLICOLOR_FORCE`, `FORCE_COLOR` is set to a non-zero value, [`Mode::Always`] is returned
225    ///
226    /// * otherwise None is returned
227    #[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]
272/// Set the global coloring mode (this allows forcing colors on or off despite stream preferences)
273pub 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/// Reads the current mode from the environment
282///
283/// if no relevant environment variables are set, then the coloring mode is left unchanged
284///
285/// see [`Mode::from_env`] for details on which env vars are supported
286#[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/// Get the global coloring mode
300///
301/// This can be set from [`set_coloring_mode`], [`set_coloring_mode_from_env`]
302/// or the feature flag `strip-colors`
303///
304/// If it is not set, this returns a value of `Mode::Detect`
305#[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/// Set the default, stream to be used as a last resort
315///
316/// for example, you may use [`Stream::NeverColor`] to disable coloring if a stream is not specified
317/// by the user and the global coloring mode is [`Mode::Detect`].
318///
319/// ```rust
320/// colorz::mode::set_default_stream(colorz::mode::Stream::NeverColor);
321/// ```
322#[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/// Get the default stream
331///
332/// if one was not set by [`set_default_stream`], then this returns [`Stream::AlwaysColor`]. Otherwise return
333/// the value specified in [`set_default_stream`]
334#[inline]
335pub fn get_default_stream() -> Stream {
336    Stream::decode(DEFAULT_STREAM.load(core::sync::atomic::Ordering::Acquire))
337}
338
339/// Should the given stream and color kinds be colored based on the coloring mode.
340///
341/// for example, you can use this to decide if you need to color based on ANSI
342///
343/// ```rust
344/// fn write_color(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
345///     use colorz::WriteColor;
346///
347///     if colorz::mode::should_color(None, &[colorz::mode::ColorKind::Ansi]) {
348///         colorz::ansi::Red.fmt_foreground(f)?;
349///     }
350///     Ok(())
351/// }
352/// ```
353///
354/// The `stream` provided may be used to detect whether it supports coloring.
355/// For example, if the `std` feature is enabled, but not the `supports-colors` feature
356/// then using `Stream::StdErr` will check if `stderr` is a terminal and allow coloring
357/// if it is a terminal.
358///
359/// # Coloring Mode
360///
361/// There are many ways to specify the coloring mode for `colorz`, and it may not be obvious how
362/// they interact, so here is a precedence list. To figure out how colorz chooses to colorz, go
363/// down the list, and the first element that applies will be selected.
364///
365/// * if the feature flag `strip-colors` is enabled -> NO COLOR
366/// * if the global coloring mode is `Mode::Always` -> DO COLOR
367/// * if the global coloring mode is `Mode::NEVER`  -> NO COLOR
368/// * if the per-value stream if set to
369///     * `Stream::AlwaysColor` -> DO COLOR
370///     * `Stream::NeverColor` -> NO COLOR
371///     * `Stream::Stdout`/`Stream::Stderr` -> detect coloring using `std` or `support-color` (see docs on feature flags for details)
372/// * if global stream is set to
373///     * `Stream::AlwaysColor` -> DO COLOR
374///     * `Stream::NeverColor` -> NO COLOR
375///     * `Stream::Stdout`/`Stream::Stderr` -> detect coloring using `std` or `support-color` (see docs on feature flags for details)
376///
377/// The global stream is always set to one of the possible `Stream` values,
378/// so one option on the list will always be chosen.
379///
380/// NOTE that setting the coloring mode from the environment sets the global coloring mode,
381/// so either the second or third option on the list.
382///
383/// NOTE that the coloring mode only affects `StyledValue` (which includes all outputs of the `Colorize` trait).
384/// Using `Style::apply`/`Style::clear` directly will not respect the coloring mode, and can be used to force
385/// coloring regardless of the current coloring mode. You can use `Style::should_color` or `mode::should_color` to detect if a given style
386/// should be used based on the current coloring mode.
387///
388/// ```rust
389/// use colorz::{Style, xterm, mode::Stream};
390///
391/// let style = Style::new().fg(xterm::Aquamarine);
392///
393/// if style.should_color(Stream::AlwaysColor) {
394///     println!("{}style if global is set{}", style.apply(), style.clear());
395/// }
396///
397/// if style.should_color(None) {
398///     println!("{}style if global is set or default stream is set{}", style.apply(), style.clear());
399/// }
400/// ```
401#[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}