Skip to main content

farben_core/
env.rs

1//! Runtime detection of whether ANSI color output should be enabled.
2//!
3//! Respects the [`NO_COLOR`](https://no-color.org) and
4//! [`FORCE_COLOR`](https://force-color.org) environment variable conventions,
5//! in that order of precedence, before falling back to TTY detection.
6//!
7//! The result is computed once per process and cached in a [`OnceLock`].
8
9use std::sync::OnceLock;
10
11static COLOR_ENABLED: OnceLock<bool> = OnceLock::new();
12
13/// Returns whether ANSI color output is enabled for this process.
14///
15/// The decision is made once and cached. Subsequent calls return the cached
16/// value without re-checking the environment.
17///
18/// # Detection order
19///
20/// 1. `NO_COLOR` set (any value) -> `false`
21/// 2. `FORCE_COLOR` set (any value) -> `true`
22/// 3. stdout is a TTY (Unix or Windows) -> `true`, otherwise `false`
23pub fn color_enabled() -> bool {
24    *COLOR_ENABLED.get_or_init(|| {
25        if std::env::var("NO_COLOR").is_ok() {
26            return false;
27        }
28
29        if std::env::var("FORCE_COLOR").is_ok() {
30            return true;
31        }
32
33        is_tty()
34    })
35}
36
37#[cfg(unix)]
38unsafe extern "C" {
39    fn isatty(fd: i32) -> i32;
40}
41
42/// Reports whether stdout is connected to a terminal.
43///
44/// - Unix: calls `isatty(1)` via the POSIX C interface.
45/// - Windows: calls `GetStdHandle(STD_OUTPUT_HANDLE)` and checks that
46///   `GetConsoleMode` succeeds, which fails on redirected handles.
47/// - All other targets: returns `false`.
48fn is_tty() -> bool {
49    #[cfg(unix)]
50    {
51        // SAFETY: fd 1 is stdout, which is always a valid open file descriptor for this process.
52        // isatty() is async-signal-safe and does not mutate any Rust-owned memory.
53        unsafe { isatty(1) != 0 }
54    }
55
56    #[cfg(all(not(unix), windows))]
57    {
58        // SAFETY: is_tty_windows only calls Win32 handle query APIs. GetStdHandle returns a
59        // pseudo-handle owned by the OS, not the caller, so it must not be closed. GetConsoleMode
60        // writes into a local u32 on the stack with no aliasing concerns.
61        unsafe { is_tty_windows() }
62    }
63
64    #[cfg(all(not(unix), not(windows)))]
65    {
66        return false;
67    }
68}
69
70#[cfg(windows)]
71unsafe extern "system" {
72    fn GetStdHandle(nStdHandle: u32) -> *mut u8;
73    fn GetConsoleMode(hConsoleHandle: *mut u8, lpMode: *mut u32) -> i32;
74}
75
76#[cfg(windows)]
77fn is_tty_windows() -> bool {
78    const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5;
79    unsafe {
80        let handle = GetStdHandle(STD_OUTPUT_HANDLE);
81        let mut mode: u32 = 0;
82        GetConsoleMode(handle, &mut mode) != 0
83    }
84}