color_nope/
lib.rs

1/*!
2Support for standard options to disable colors in the terminal.
3
4An implementation of the [NO_COLOR](https://no-color.org/) standard, following
5the [Command Line Interface Guidelines](https://clig.dev/#output).
6
7## Usage
8
9See [`ColorNope`] for usage examples.
10*/
11
12#![deny(missing_docs)]
13
14#[cfg(doctest)]
15use doc_comment::doctest;
16#[cfg(doctest)]
17doctest!("../README.md");
18
19use std::ffi::OsString;
20
21/// Decides whether color should be enabled, based on the environment and the
22/// target stream.
23///
24/// Assumes color is enabled by default, unless indicated otherwise.
25///
26/// # Examples
27///
28/// Can be created using the `from_env()` convenience function:
29///
30/// ```rust
31/// use color_nope::{ColorNope, Stream};
32///
33/// assert_eq!(
34///     ColorNope::from_env().enable_color_for(Stream::Stdout),
35///     false
36/// );
37/// ```
38///
39/// Or by passing in your own values:
40///
41/// ```rust
42/// use color_nope::{ColorNope, Stream, Force};
43///
44/// assert_eq!(
45///     ColorNope::new(
46///         std::env::var_os("TERM"),
47///         std::env::var_os("NO_COLOR"),
48///         if std::env::args_os().any(|a| a == "--no-color") {
49///             Some(Force::Off)
50///         } else {
51///             None
52///         },
53///     )
54///     .enable_color_for(Stream::Stdout),
55///     false
56/// );
57/// ```
58#[derive(Clone, Debug)]
59pub struct ColorNope {
60    term_env: Option<OsString>,
61    no_color_env: Option<OsString>,
62    force_color: Option<Force>,
63}
64
65impl ColorNope {
66    /// Create a new instance without touching the environment.
67    ///
68    /// [`ColorNope`] considers the `TERM` and `NO_COLOR` environmental
69    /// variables (`term_env` and `no_color_env` respectively).
70    ///
71    /// These values can be overridden by using `force_color`.
72    ///
73    /// # Example
74    ///
75    /// ```rust
76    /// # use color_nope::ColorNope;
77    /// ColorNope::new(
78    ///     std::env::var_os("TERM"),
79    ///     std::env::var_os("NO_COLOR"),
80    ///     None
81    /// );
82    /// ```
83    pub fn new(
84        term_env: Option<OsString>,
85        no_color_env: Option<OsString>,
86        force_color: Option<Force>,
87    ) -> ColorNope {
88        ColorNope {
89            term_env,
90            no_color_env,
91            force_color,
92        }
93    }
94
95    /// Uses the `TERM` and `NO_COLOR` environmental variables.
96    pub fn from_env() -> ColorNope {
97        ColorNope {
98            term_env: std::env::var_os("TERM"),
99            no_color_env: std::env::var_os("NO_COLOR"),
100            force_color: None,
101        }
102    }
103
104    /// Should color be enabled for the target stream?
105    pub fn enable_color_for(&self, stream: Stream) -> bool {
106        match self.force_color {
107            Some(force) => force.enable_color(),
108            None => {
109                atty::is(stream.into())
110                    && term_allows_color(self.term_env.as_ref())
111                    && no_color_allows_color(self.no_color_env.as_ref())
112            }
113        }
114    }
115}
116
117/// Output streams.
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub enum Stream {
120    #[allow(missing_docs)]
121    Stdout,
122    #[allow(missing_docs)]
123    Stderr,
124}
125impl From<Stream> for atty::Stream {
126    fn from(s: Stream) -> Self {
127        match s {
128            Stream::Stdout => atty::Stream::Stdout,
129            Stream::Stderr => atty::Stream::Stderr,
130        }
131    }
132}
133
134/// Override other settings to force colors on or off.
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum Force {
137    #[allow(missing_docs)]
138    On,
139    #[allow(missing_docs)]
140    Off,
141}
142impl Force {
143    fn enable_color(&self) -> bool {
144        use Force::*;
145        match self {
146            On => true,
147            Off => false,
148        }
149    }
150}
151
152fn no_color_allows_color(no_color: Option<&OsString>) -> bool {
153    no_color.map_or(true, |s| s.is_empty())
154}
155
156// These next functions are shamelessly stolen from [termcolor](https://github.com/BurntSushi/termcolor).
157
158#[cfg(not(windows))]
159fn term_allows_color(term: Option<&OsString>) -> bool {
160    match term {
161        // If TERM isn't set, then we are in a weird environment that
162        // probably doesn't support colors.
163        None => false,
164        Some(v) => v != "dumb",
165    }
166}
167
168#[cfg(windows)]
169fn term_allows_color(term: Option<&OsString>) -> bool {
170    // On Windows, if TERM isn't set, then we shouldn't automatically
171    // assume that colors aren't allowed. This is unlike Unix environments
172    // where TERM is more rigorously set.
173    if let Some(v) = term {
174        v != "dumb"
175    } else {
176        true
177    }
178}