openlatch-provider 0.2.1

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
//! TTY / color / CI environment detection.
//!
//! `NO_COLOR=1` and `--no-color` strip ANSI; `FORCE_COLOR=1` overrides to
//! always-on. `CI=truthy` (or any of the well-known CI flags) auto-disables
//! interactive prompts, spinners, and progress bars.
//!
//! Cloned in spirit from openlatch-client `src/cli/color.rs` + extended for
//! the openlatch-provider 4-mode output dispatch.

use std::io::IsTerminal;

/// Color preference resolved from CLI flag + env.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ColorChoice {
    #[default]
    Auto,
    Always,
    Never,
}

/// Resolve the color choice using the documented precedence:
/// `--no-color` flag > `NO_COLOR` env > `FORCE_COLOR` env > TTY auto-detect.
pub fn resolve_color(no_color_flag: bool) -> ColorChoice {
    if no_color_flag {
        return ColorChoice::Never;
    }
    if std::env::var_os("NO_COLOR").is_some() {
        return ColorChoice::Never;
    }
    if std::env::var_os("FORCE_COLOR").is_some() {
        return ColorChoice::Always;
    }
    ColorChoice::Auto
}

/// Whether stdout is connected to a real terminal.
pub fn stdout_is_tty() -> bool {
    std::io::stdout().is_terminal()
}

/// Whether stderr is connected to a real terminal.
pub fn stderr_is_tty() -> bool {
    std::io::stderr().is_terminal()
}

/// Whether we are running inside a CI provider — used to suppress spinners
/// and force non-interactive defaults.
pub fn is_ci() -> bool {
    const KEYS: &[&str] = &[
        "CI",
        "GITHUB_ACTIONS",
        "GITLAB_CI",
        "CIRCLECI",
        "JENKINS_URL",
        "BUILDKITE",
        "TF_BUILD", // Azure Pipelines
    ];
    for k in KEYS {
        if let Some(v) = std::env::var_os(k) {
            // Treat empty string as "not set"; some tooling exports CI="".
            if !v.is_empty() {
                return true;
            }
        }
    }
    false
}

/// Whether interactive prompts should be allowed.
///
/// Returns `false` when:
/// - `--non-interactive` is set,
/// - stdin is not a TTY,
/// - we're inside CI.
pub fn allow_interactive(non_interactive_flag: bool) -> bool {
    if non_interactive_flag || is_ci() {
        return false;
    }
    std::io::stdin().is_terminal()
}

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

    #[test]
    fn no_color_flag_wins() {
        // We don't unset env here; the function should short-circuit before
        // touching env state.
        assert_eq!(resolve_color(true), ColorChoice::Never);
    }
}