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 {
Self::detect_with_env(|var| env::var(var).ok(), std::io::stdout().is_terminal())
}
#[must_use]
pub fn detect_with_env<F>(env_lookup: F, is_terminal: bool) -> Self
where
F: Fn(&str) -> Option<String>,
{
let is_truthy = |var: &str| -> bool {
env_lookup(var).is_some_and(|val| {
let v = val.trim().to_lowercase();
v == "1" || v == "true" || v == "yes" || v == "on"
})
};
if is_truthy("SQLMODEL_PLAIN") {
return Self::Plain;
}
if is_truthy("SQLMODEL_JSON") {
return Self::Json;
}
if is_truthy("SQLMODEL_RICH") {
return Self::Rich; }
if env_lookup("NO_COLOR").is_some() {
return Self::Plain;
}
if is_truthy("CI") {
return Self::Plain;
}
if env_lookup("TERM").is_some_and(|t| t == "dumb") {
return Self::Plain;
}
if Self::is_agent_environment_with(&env_lookup) {
return Self::Plain;
}
if !is_terminal {
return Self::Plain;
}
Self::Rich
}
#[must_use]
pub fn is_agent_environment() -> bool {
Self::is_agent_environment_with(|var| env::var(var).ok())
}
#[must_use]
pub fn is_agent_environment_with<F>(env_lookup: F) -> bool
where
F: Fn(&str) -> Option<String>,
{
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_lookup(var).is_some())
}
#[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())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_env(vars: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> + use<> {
let owned: Vec<(String, String)> = vars
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
move |key| owned.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
#[test]
fn test_default_is_rich() {
assert_eq!(OutputMode::default(), OutputMode::Rich);
}
#[test]
fn test_explicit_plain_override() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("SQLMODEL_PLAIN", "1")]), true),
OutputMode::Plain
);
}
#[test]
fn test_explicit_plain_override_true() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("SQLMODEL_PLAIN", "true")]), true),
OutputMode::Plain
);
}
#[test]
fn test_explicit_json_override() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("SQLMODEL_JSON", "1")]), true),
OutputMode::Json
);
}
#[test]
fn test_explicit_rich_override() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("SQLMODEL_RICH", "1")]), false),
OutputMode::Rich
);
}
#[test]
fn test_plain_takes_priority_over_json() {
assert_eq!(
OutputMode::detect_with_env(
mock_env(&[("SQLMODEL_PLAIN", "1"), ("SQLMODEL_JSON", "1")]),
true
),
OutputMode::Plain
);
}
#[test]
fn test_agent_detection_claude() {
assert!(OutputMode::is_agent_environment_with(mock_env(&[(
"CLAUDE_CODE",
"1"
)])));
}
#[test]
fn test_agent_detection_codex() {
assert!(OutputMode::is_agent_environment_with(mock_env(&[(
"CODEX_CLI",
"1"
)])));
}
#[test]
fn test_agent_detection_cursor() {
assert!(OutputMode::is_agent_environment_with(mock_env(&[(
"CURSOR_SESSION",
"active"
)])));
}
#[test]
fn test_agent_detection_aider() {
assert!(OutputMode::is_agent_environment_with(mock_env(&[(
"AIDER_MODEL",
"gpt-4"
)])));
}
#[test]
fn test_agent_causes_plain_mode() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("CLAUDE_CODE", "1")]), true),
OutputMode::Plain
);
}
#[test]
fn test_rich_override_beats_agent() {
assert_eq!(
OutputMode::detect_with_env(
mock_env(&[("CLAUDE_CODE", "1"), ("SQLMODEL_RICH", "1")]),
true
),
OutputMode::Rich
);
}
#[test]
fn test_no_color_causes_plain() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("NO_COLOR", "")]), true),
OutputMode::Plain
);
}
#[test]
fn test_ci_causes_plain() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("CI", "true")]), true),
OutputMode::Plain
);
}
#[test]
fn test_dumb_terminal_causes_plain() {
assert_eq!(
OutputMode::detect_with_env(mock_env(&[("TERM", "dumb")]), true),
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() {
let empty = mock_env(&[]);
assert_eq!(OutputMode::detect_with_env(&empty, true), OutputMode::Rich);
for truthy in ["1", "true", "TRUE", "yes", "on"] {
let env = mock_env(&[("SQLMODEL_PLAIN", truthy)]);
assert_eq!(
OutputMode::detect_with_env(&env, true),
OutputMode::Plain,
"truthy check failed for {truthy}"
);
}
for falsy in ["0", "false", "no", "off", ""] {
let env = mock_env(&[("SQLMODEL_PLAIN", falsy)]);
assert_eq!(
OutputMode::detect_with_env(&env, true),
OutputMode::Rich,
"falsy check failed for {falsy}"
);
}
}
#[test]
fn test_no_agent_when_clean() {
assert!(!OutputMode::is_agent_environment_with(mock_env(&[])));
}
}