use std::env;
use std::io::IsTerminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum OutputMode {
Plain,
#[default]
Rich,
Json,
}
impl OutputMode {
#[must_use]
pub fn detect() -> Self {
if env_is_truthy("SQLMODEL_PLAIN") {
return Self::Plain;
}
if env_is_truthy("SQLMODEL_JSON") {
return Self::Json;
}
if env_is_truthy("SQLMODEL_RICH") {
return Self::Rich; }
if env::var("NO_COLOR").is_ok() {
return Self::Plain;
}
if env_is_truthy("CI") {
return Self::Plain;
}
if env::var("TERM").is_ok_and(|t| t == "dumb") {
return Self::Plain;
}
if Self::is_agent_environment() {
return Self::Plain;
}
if !std::io::stdout().is_terminal() {
return Self::Plain;
}
Self::Rich
}
#[must_use]
pub fn is_agent_environment() -> bool {
const AGENT_MARKERS: &[&str] = &[
"CLAUDE_CODE",
"CODEX_CLI",
"CODEX_SESSION",
"CURSOR_SESSION",
"CURSOR_EDITOR",
"AIDER_MODEL",
"AIDER_REPO",
"AGENT_MODE",
"AI_AGENT",
"GITHUB_COPILOT",
"COPILOT_SESSION",
"CONTINUE_SESSION",
"CODY_AGENT",
"CODY_SESSION",
"WINDSURF_SESSION",
"CODEIUM_AGENT",
"GEMINI_CLI",
"GEMINI_SESSION",
"CODEWHISPERER_SESSION",
"AMAZON_Q_SESSION",
];
AGENT_MARKERS.iter().any(|var| env::var(var).is_ok())
}
#[must_use]
pub const fn supports_ansi(&self) -> bool {
matches!(self, Self::Rich)
}
#[must_use]
pub const fn is_structured(&self) -> bool {
matches!(self, Self::Json)
}
#[must_use]
pub const fn is_plain(&self) -> bool {
matches!(self, Self::Plain)
}
#[must_use]
pub const fn is_rich(&self) -> bool {
matches!(self, Self::Rich)
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Plain => "plain",
Self::Rich => "rich",
Self::Json => "json",
}
}
}
impl std::fmt::Display for OutputMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
fn env_is_truthy(name: &str) -> bool {
env::var(name).is_ok_and(|v| {
let v = v.to_lowercase();
v == "1" || v == "true" || v == "yes" || v == "on"
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
const VARS_TO_CLEAR: &[&str] = &[
"SQLMODEL_PLAIN",
"SQLMODEL_JSON",
"SQLMODEL_RICH",
"NO_COLOR",
"CI",
"TERM",
"CLAUDE_CODE",
"CODEX_CLI",
"CURSOR_SESSION",
"AIDER_MODEL",
"AGENT_MODE",
"GITHUB_COPILOT",
"CONTINUE_SESSION",
];
#[allow(unsafe_code)]
fn test_set_var(key: &str, value: &str) {
unsafe { env::set_var(key, value) };
}
#[allow(unsafe_code)]
fn test_remove_var(key: &str) {
unsafe { env::remove_var(key) };
}
fn with_clean_env<F: FnOnce()>(f: F) {
let saved: Vec<_> = VARS_TO_CLEAR
.iter()
.map(|&v| (v, env::var(v).ok()))
.collect();
for &var in VARS_TO_CLEAR {
test_remove_var(var);
}
f();
for (var, val) in saved {
match val {
Some(v) => test_set_var(var, &v),
None => test_remove_var(var),
}
}
}
#[test]
fn test_default_is_rich() {
assert_eq!(OutputMode::default(), OutputMode::Rich);
}
#[test]
fn test_explicit_plain_override() {
with_clean_env(|| {
test_set_var("SQLMODEL_PLAIN", "1");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
fn test_explicit_plain_override_true() {
with_clean_env(|| {
test_set_var("SQLMODEL_PLAIN", "true");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_explicit_json_override() {
with_clean_env(|| {
test_set_var("SQLMODEL_JSON", "1");
assert_eq!(OutputMode::detect(), OutputMode::Json);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests (CI sets CI=true)"]
fn test_explicit_rich_override() {
with_clean_env(|| {
test_set_var("SQLMODEL_RICH", "1");
assert_eq!(OutputMode::detect(), OutputMode::Rich);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_plain_takes_priority_over_json() {
with_clean_env(|| {
test_set_var("SQLMODEL_PLAIN", "1");
test_set_var("SQLMODEL_JSON", "1");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_agent_detection_claude() {
with_clean_env(|| {
test_set_var("CLAUDE_CODE", "1");
assert!(OutputMode::is_agent_environment());
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_agent_detection_codex() {
with_clean_env(|| {
test_set_var("CODEX_CLI", "1");
assert!(OutputMode::is_agent_environment());
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_agent_detection_cursor() {
with_clean_env(|| {
test_set_var("CURSOR_SESSION", "active");
assert!(OutputMode::is_agent_environment());
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_agent_detection_aider() {
with_clean_env(|| {
test_set_var("AIDER_MODEL", "gpt-4");
assert!(OutputMode::is_agent_environment());
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_agent_causes_plain_mode() {
with_clean_env(|| {
test_set_var("CLAUDE_CODE", "1");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests (CI sets CI=true)"]
fn test_rich_override_beats_agent() {
with_clean_env(|| {
test_set_var("CLAUDE_CODE", "1");
test_set_var("SQLMODEL_RICH", "1");
assert_eq!(OutputMode::detect(), OutputMode::Rich);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_no_color_causes_plain() {
with_clean_env(|| {
test_set_var("NO_COLOR", "");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_ci_causes_plain() {
with_clean_env(|| {
test_set_var("CI", "true");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_dumb_terminal_causes_plain() {
with_clean_env(|| {
test_set_var("TERM", "dumb");
assert_eq!(OutputMode::detect(), OutputMode::Plain);
});
}
#[test]
fn test_supports_ansi() {
assert!(!OutputMode::Plain.supports_ansi());
assert!(OutputMode::Rich.supports_ansi());
assert!(!OutputMode::Json.supports_ansi());
}
#[test]
fn test_is_structured() {
assert!(!OutputMode::Plain.is_structured());
assert!(!OutputMode::Rich.is_structured());
assert!(OutputMode::Json.is_structured());
}
#[test]
fn test_is_plain() {
assert!(OutputMode::Plain.is_plain());
assert!(!OutputMode::Rich.is_plain());
assert!(!OutputMode::Json.is_plain());
}
#[test]
fn test_is_rich() {
assert!(!OutputMode::Plain.is_rich());
assert!(OutputMode::Rich.is_rich());
assert!(!OutputMode::Json.is_rich());
}
#[test]
fn test_as_str() {
assert_eq!(OutputMode::Plain.as_str(), "plain");
assert_eq!(OutputMode::Rich.as_str(), "rich");
assert_eq!(OutputMode::Json.as_str(), "json");
}
#[test]
fn test_display() {
assert_eq!(format!("{}", OutputMode::Plain), "plain");
assert_eq!(format!("{}", OutputMode::Rich), "rich");
assert_eq!(format!("{}", OutputMode::Json), "json");
}
#[test]
fn test_env_is_truthy() {
with_clean_env(|| {
assert!(!env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "1");
assert!(env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "true");
assert!(env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "TRUE");
assert!(env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "yes");
assert!(env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "on");
assert!(env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "0");
assert!(!env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "false");
assert!(!env_is_truthy("SQLMODEL_TEST_VAR"));
test_set_var("SQLMODEL_TEST_VAR", "");
assert!(!env_is_truthy("SQLMODEL_TEST_VAR"));
test_remove_var("SQLMODEL_TEST_VAR");
});
}
#[test]
#[ignore = "flaky: env var race conditions in parallel tests"]
fn test_no_agent_when_clean() {
with_clean_env(|| {
assert!(!OutputMode::is_agent_environment());
});
}
}