Skip to main content

vtcode_commons/
color_policy.rs

1#![expect(
2    unused_results,
3    reason = "Forcing the one-time environment initialization is intentionally used only for its side effect."
4)]
5
6//! Runtime color output policy helpers.
7//!
8//! This module centralizes color enable/disable decisions for CLI and
9//! transcript-style output paths. By default it follows the NO_COLOR
10//! environment variable with strict "present and non-empty" semantics.
11
12use once_cell::sync::Lazy;
13use std::ffi::OsString;
14use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
15
16/// Source that determined the active runtime color policy.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum ColorOutputPolicySource {
19    /// Default runtime behavior (auto detect + env hints).
20    DefaultAuto,
21    /// Disabled due to NO_COLOR environment variable.
22    NoColorEnv,
23    /// Disabled due to explicit `--no-color`.
24    CliNoColor,
25    /// Disabled due to explicit `--color never`.
26    CliColorNever,
27    /// Enabled due to explicit `--color always`.
28    CliColorAlways,
29    /// Enabled or disabled by explicit config override.
30    ConfigOverride,
31}
32
33/// Runtime color output policy.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct ColorOutputPolicy {
36    pub enabled: bool,
37    pub source: ColorOutputPolicySource,
38}
39
40const SOURCE_DEFAULT_AUTO: u8 = 0;
41const SOURCE_NO_COLOR_ENV: u8 = 1;
42const SOURCE_CLI_NO_COLOR: u8 = 2;
43const SOURCE_CLI_COLOR_NEVER: u8 = 3;
44const SOURCE_CLI_COLOR_ALWAYS: u8 = 4;
45const SOURCE_CONFIG_OVERRIDE: u8 = 5;
46
47static POLICY_ENABLED: AtomicBool = AtomicBool::new(true);
48static POLICY_SOURCE: AtomicU8 = AtomicU8::new(SOURCE_DEFAULT_AUTO);
49
50static INIT_FROM_ENV: Lazy<()> = Lazy::new(|| {
51    let default_policy = detect_policy_from_env();
52    set_color_output_policy(default_policy);
53});
54
55fn detect_policy_from_env() -> ColorOutputPolicy {
56    if no_color_env_active() {
57        ColorOutputPolicy {
58            enabled: false,
59            source: ColorOutputPolicySource::NoColorEnv,
60        }
61    } else {
62        ColorOutputPolicy {
63            enabled: true,
64            source: ColorOutputPolicySource::DefaultAuto,
65        }
66    }
67}
68
69fn encode_source(source: ColorOutputPolicySource) -> u8 {
70    match source {
71        ColorOutputPolicySource::DefaultAuto => SOURCE_DEFAULT_AUTO,
72        ColorOutputPolicySource::NoColorEnv => SOURCE_NO_COLOR_ENV,
73        ColorOutputPolicySource::CliNoColor => SOURCE_CLI_NO_COLOR,
74        ColorOutputPolicySource::CliColorNever => SOURCE_CLI_COLOR_NEVER,
75        ColorOutputPolicySource::CliColorAlways => SOURCE_CLI_COLOR_ALWAYS,
76        ColorOutputPolicySource::ConfigOverride => SOURCE_CONFIG_OVERRIDE,
77    }
78}
79
80fn decode_source(value: u8) -> ColorOutputPolicySource {
81    match value {
82        SOURCE_NO_COLOR_ENV => ColorOutputPolicySource::NoColorEnv,
83        SOURCE_CLI_NO_COLOR => ColorOutputPolicySource::CliNoColor,
84        SOURCE_CLI_COLOR_NEVER => ColorOutputPolicySource::CliColorNever,
85        SOURCE_CLI_COLOR_ALWAYS => ColorOutputPolicySource::CliColorAlways,
86        SOURCE_CONFIG_OVERRIDE => ColorOutputPolicySource::ConfigOverride,
87        _ => ColorOutputPolicySource::DefaultAuto,
88    }
89}
90
91fn no_color_env_active_from(value: Option<OsString>) -> bool {
92    value.map(|v| !v.is_empty()).unwrap_or(false)
93}
94
95/// Returns true when NO_COLOR is present and non-empty.
96#[must_use]
97pub fn no_color_env_active() -> bool {
98    no_color_env_active_from(std::env::var_os("NO_COLOR"))
99}
100
101/// Read the current runtime color policy.
102pub fn current_color_output_policy() -> ColorOutputPolicy {
103    Lazy::force(&INIT_FROM_ENV);
104    ColorOutputPolicy {
105        enabled: POLICY_ENABLED.load(Ordering::Relaxed),
106        source: decode_source(POLICY_SOURCE.load(Ordering::Relaxed)),
107    }
108}
109
110/// Replace the current runtime color policy.
111pub fn set_color_output_policy(policy: ColorOutputPolicy) {
112    POLICY_ENABLED.store(policy.enabled, Ordering::Relaxed);
113    POLICY_SOURCE.store(encode_source(policy.source), Ordering::Relaxed);
114}
115
116/// Reset runtime color policy from environment defaults.
117pub fn reset_color_output_policy_from_env() {
118    set_color_output_policy(detect_policy_from_env());
119}
120
121/// Returns true when runtime color output is enabled.
122#[must_use]
123pub fn color_output_enabled() -> bool {
124    current_color_output_policy().enabled
125}
126
127#[cfg(test)]
128mod tests {
129    use super::no_color_env_active_from;
130    use std::ffi::OsString;
131
132    #[test]
133    fn no_color_requires_non_empty_value() {
134        assert!(!no_color_env_active_from(None));
135        assert!(!no_color_env_active_from(Some(OsString::from(""))));
136        assert!(no_color_env_active_from(Some(OsString::from("1"))));
137    }
138}