1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! Low level terminal capability lookups

#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs)]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]

pub mod windows;

/// Check [CLICOLOR] status
///
/// - When `true`, ANSI colors are supported and should be used when the program isn't piped,
///   similar to [`term_supports_color`]
/// - When `false`, don’t output ANSI color escape codes, similar to [`no_color`]
///
/// See also:
/// - [terminfo](https://crates.io/crates/terminfo) or [term](https://crates.io/crates/term) for
///   checking termcaps
/// - [termbg](https://crates.io/crates/termbg) for detecting background color
///
/// [CLICOLOR]: https://bixense.com/clicolors/
#[inline]
pub fn clicolor() -> Option<bool> {
    let value = std::env::var_os("CLICOLOR")?;
    Some(value != "0")
}

/// Check [CLICOLOR_FORCE] status
///
/// ANSI colors should be enabled no matter what.
///
/// [CLICOLOR_FORCE]: https://bixense.com/clicolors/
#[inline]
pub fn clicolor_force() -> bool {
    non_empty(std::env::var_os("CLICOLOR_FORCE").as_deref())
}

/// Check [NO_COLOR] status
///
/// When `true`, should prevent the addition of ANSI color.
///
/// User-level configuration files and per-instance command-line arguments should override
/// [NO_COLOR]. A user should be able to export `$NO_COLOR` in their shell configuration file as a
/// default, but configure a specific program in its configuration file to specifically enable
/// color.
///
/// [NO_COLOR]: https://no-color.org/
#[inline]
pub fn no_color() -> bool {
    non_empty(std::env::var_os("NO_COLOR").as_deref())
}

/// Check `TERM` for color support
#[inline]
#[cfg(not(windows))]
pub fn term_supports_color() -> bool {
    match std::env::var_os("TERM") {
        // If TERM isn't set, then we are in a weird environment that
        // probably doesn't support colors.
        None => return false,
        Some(k) => {
            if k == "dumb" {
                return false;
            }
        }
    }
    true
}

/// Check `TERM` for color support
#[inline]
#[cfg(windows)]
pub fn term_supports_color() -> bool {
    // On Windows, if TERM isn't set, then we shouldn't automatically
    // assume that colors aren't allowed. This is unlike Unix environments
    // where TERM is more rigorously set.
    if let Some(k) = std::env::var_os("TERM") {
        if k == "dumb" {
            return false;
        }
    }
    true
}

/// Check `TERM` for ANSI color support
#[inline]
#[cfg(not(windows))]
pub fn term_supports_ansi_color() -> bool {
    term_supports_color()
}

/// Check `TERM` for ANSI color support
#[inline]
#[cfg(windows)]
pub fn term_supports_ansi_color() -> bool {
    match std::env::var_os("TERM") {
        // If TERM isn't set, then we are in a weird environment that
        // probably doesn't support ansi.
        None => return false,
        Some(k) => {
            // cygwin doesn't seem to support ANSI escape sequences
            // and instead has its own variety. However, the Windows
            // console API may be available.
            if k == "dumb" || k == "cygwin" {
                return false;
            }
        }
    }
    true
}

/// Check [COLORTERM] for truecolor support
///
/// [COLORTERM]: https://github.com/termstandard/colors
#[inline]
pub fn truecolor() -> bool {
    let value = std::env::var_os("COLORTERM");
    let value = value.as_deref().unwrap_or_default();
    value == "truecolor" || value == "24bit"
}

/// Report whether this is running in CI
///
/// CI is a common environment where, despite being piped, ansi color codes are supported
///
/// This is not as exhaustive as you'd find in a crate like `is_ci` but it should work in enough
/// cases.
#[inline]
pub fn is_ci() -> bool {
    // Assuming its CI based on presence because who would be setting `CI=false`?
    //
    // This makes it easier to all of the potential values when considering our known values:
    // - Gitlab and Github set it to `true`
    // - Woodpecker sets it to `woodpecker`
    std::env::var_os("CI").is_some()
}

fn non_empty(var: Option<&std::ffi::OsStr>) -> bool {
    !var.unwrap_or_default().is_empty()
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn non_empty_not_present() {
        assert!(!non_empty(None));
    }

    #[test]
    fn non_empty_empty() {
        assert!(!non_empty(Some(std::ffi::OsStr::new(""))));
    }

    #[test]
    fn non_empty_texty() {
        assert!(non_empty(Some(std::ffi::OsStr::new("hello"))));
    }
}