use crate::detection::{OutputPreference, detected_preference};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputMode {
Rich,
#[default]
Plain,
Minimal,
}
impl OutputMode {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Rich => "rich",
Self::Plain => "plain",
Self::Minimal => "minimal",
}
}
#[must_use]
pub const fn is_agent_friendly(&self) -> bool {
matches!(self, Self::Plain)
}
#[must_use]
pub fn auto() -> Self {
if let Ok(mode_str) = std::env::var("FASTAPI_OUTPUT_MODE") {
if let Ok(mode) = mode_str.parse::<OutputMode>() {
if matches!(mode, OutputMode::Rich) {
#[cfg(feature = "rich")]
{
return OutputMode::Rich;
}
#[cfg(not(feature = "rich"))]
{
return OutputMode::Plain;
}
}
return mode;
}
}
match detected_preference() {
OutputPreference::Plain => OutputMode::Plain,
OutputPreference::Rich => {
#[cfg(feature = "rich")]
{
OutputMode::Rich
}
#[cfg(not(feature = "rich"))]
{
OutputMode::Plain
}
}
}
}
#[must_use]
pub const fn uses_colors(&self) -> bool {
matches!(self, Self::Rich | Self::Minimal)
}
#[must_use]
pub const fn uses_boxes(&self) -> bool {
matches!(self, Self::Rich)
}
#[must_use]
pub const fn supports_tables(&self) -> bool {
matches!(self, Self::Rich)
}
#[must_use]
pub const fn success_indicator(&self) -> &'static str {
match self {
Self::Rich => "✓",
Self::Plain | Self::Minimal => "[OK]",
}
}
#[must_use]
pub const fn error_indicator(&self) -> &'static str {
match self {
Self::Rich => "✗",
Self::Plain | Self::Minimal => "[ERROR]",
}
}
#[must_use]
pub const fn warning_indicator(&self) -> &'static str {
match self {
Self::Rich => "âš ",
Self::Plain | Self::Minimal => "[WARN]",
}
}
#[must_use]
pub const fn info_indicator(&self) -> &'static str {
match self {
Self::Rich => "ℹ",
Self::Plain | Self::Minimal => "[INFO]",
}
}
#[must_use]
pub const fn uses_ansi(&self) -> bool {
self.uses_colors()
}
#[must_use]
pub const fn is_minimal(&self) -> bool {
matches!(self, Self::Minimal)
}
}
impl std::fmt::Display for OutputMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Rich => write!(f, "rich"),
Self::Plain => write!(f, "plain"),
Self::Minimal => write!(f, "minimal"),
}
}
}
impl FromStr for OutputMode {
type Err = OutputModeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized = s.trim().to_ascii_lowercase();
match normalized.as_str() {
"rich" => Ok(Self::Rich),
"plain" => Ok(Self::Plain),
"minimal" => Ok(Self::Minimal),
_ => Err(OutputModeParseError(s.to_string())),
}
}
}
#[must_use]
pub const fn has_rich_support() -> bool {
cfg!(feature = "rich")
}
#[must_use]
pub fn feature_info() -> &'static str {
if cfg!(feature = "full") {
"full (rich output with syntax highlighting)"
} else if cfg!(feature = "rich") {
"rich (styled output with tables and panels)"
} else {
"plain (text only, no dependencies)"
}
}
#[derive(Debug, Clone)]
pub struct OutputModeParseError(String);
impl std::fmt::Display for OutputModeParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"invalid output mode '{}', expected: rich, plain, minimal",
self.0
)
}
}
impl std::error::Error for OutputModeParseError {}
#[cfg(test)]
#[allow(unsafe_code)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
fn clean_env() {
unsafe {
env::remove_var("FASTAPI_OUTPUT_MODE");
env::remove_var("FASTAPI_AGENT_MODE");
env::remove_var("FASTAPI_HUMAN_MODE");
env::remove_var("CLAUDE_CODE");
env::remove_var("FORCE_COLOR");
env::remove_var("NO_COLOR");
env::remove_var("CI");
}
}
fn with_clean_env<F: FnOnce()>(f: F) {
clean_env();
f();
clean_env();
}
fn set_env(key: &str, value: &str) {
unsafe {
env::set_var(key, value);
}
}
#[test]
fn test_output_mode_default() {
let mode = OutputMode::default();
eprintln!("[TEST] Default OutputMode: {mode:?}");
assert_eq!(mode, OutputMode::Plain);
}
#[test]
fn test_output_mode_clone_copy() {
let mode = OutputMode::Rich;
let cloned = mode;
let copied = mode;
eprintln!(
"[TEST] Clone/Copy test: original={mode:?}, cloned={cloned:?}, copied={copied:?}"
);
assert_eq!(mode, cloned);
assert_eq!(mode, copied);
}
#[test]
fn test_output_mode_equality() {
assert_eq!(OutputMode::Rich, OutputMode::Rich);
assert_eq!(OutputMode::Plain, OutputMode::Plain);
assert_eq!(OutputMode::Minimal, OutputMode::Minimal);
assert_ne!(OutputMode::Rich, OutputMode::Plain);
assert_ne!(OutputMode::Plain, OutputMode::Minimal);
}
#[test]
fn test_display_rich() {
let s = OutputMode::Rich.to_string();
eprintln!("[TEST] Display Rich: {s}");
assert_eq!(s, "rich");
}
#[test]
fn test_display_plain() {
let s = OutputMode::Plain.to_string();
eprintln!("[TEST] Display Plain: {s}");
assert_eq!(s, "plain");
}
#[test]
fn test_display_minimal() {
let s = OutputMode::Minimal.to_string();
eprintln!("[TEST] Display Minimal: {s}");
assert_eq!(s, "minimal");
}
#[test]
fn test_parse_rich() {
let mode: OutputMode = "rich".parse().unwrap();
eprintln!("[TEST] Parse rich: {mode:?}");
assert_eq!(mode, OutputMode::Rich);
}
#[test]
fn test_parse_plain() {
let mode: OutputMode = "plain".parse().unwrap();
eprintln!("[TEST] Parse plain: {mode:?}");
assert_eq!(mode, OutputMode::Plain);
}
#[test]
fn test_parse_minimal() {
let mode: OutputMode = "minimal".parse().unwrap();
eprintln!("[TEST] Parse minimal: {mode:?}");
assert_eq!(mode, OutputMode::Minimal);
}
#[test]
fn test_parse_case_insensitive() {
assert_eq!("RICH".parse::<OutputMode>().unwrap(), OutputMode::Rich);
assert_eq!("Plain".parse::<OutputMode>().unwrap(), OutputMode::Plain);
assert_eq!(
"MINIMAL".parse::<OutputMode>().unwrap(),
OutputMode::Minimal
);
eprintln!("[TEST] Case insensitive parsing works");
}
#[test]
fn test_parse_invalid() {
let result = "invalid".parse::<OutputMode>();
eprintln!("[TEST] Parse invalid: {result:?}");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("invalid"));
}
#[test]
fn test_uses_colors() {
eprintln!(
"[TEST] uses_colors: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.uses_colors(),
OutputMode::Plain.uses_colors(),
OutputMode::Minimal.uses_colors()
);
assert!(OutputMode::Rich.uses_colors());
assert!(!OutputMode::Plain.uses_colors());
assert!(OutputMode::Minimal.uses_colors());
}
#[test]
fn test_uses_boxes() {
eprintln!(
"[TEST] uses_boxes: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.uses_boxes(),
OutputMode::Plain.uses_boxes(),
OutputMode::Minimal.uses_boxes()
);
assert!(OutputMode::Rich.uses_boxes());
assert!(!OutputMode::Plain.uses_boxes());
assert!(!OutputMode::Minimal.uses_boxes());
}
#[test]
fn test_supports_tables() {
eprintln!(
"[TEST] supports_tables: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.supports_tables(),
OutputMode::Plain.supports_tables(),
OutputMode::Minimal.supports_tables()
);
assert!(OutputMode::Rich.supports_tables());
assert!(!OutputMode::Plain.supports_tables());
assert!(!OutputMode::Minimal.supports_tables());
}
#[test]
fn test_feature_info_matches_flags() {
let info = feature_info();
eprintln!("[TEST] feature_info: {info}");
if cfg!(feature = "full") {
assert!(info.contains("full"));
} else if cfg!(feature = "rich") {
assert!(info.contains("rich"));
} else {
assert!(info.contains("plain"));
}
}
#[test]
fn test_has_rich_support_flag() {
let expected = cfg!(feature = "rich");
eprintln!(
"[TEST] has_rich_support: expected={}, actual={}",
expected,
has_rich_support()
);
assert_eq!(has_rich_support(), expected);
}
#[test]
fn test_success_indicators() {
eprintln!(
"[TEST] success_indicator: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.success_indicator(),
OutputMode::Plain.success_indicator(),
OutputMode::Minimal.success_indicator()
);
assert_eq!(OutputMode::Rich.success_indicator(), "✓");
assert_eq!(OutputMode::Plain.success_indicator(), "[OK]");
assert_eq!(OutputMode::Minimal.success_indicator(), "[OK]");
}
#[test]
fn test_error_indicators() {
eprintln!(
"[TEST] error_indicator: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.error_indicator(),
OutputMode::Plain.error_indicator(),
OutputMode::Minimal.error_indicator()
);
assert_eq!(OutputMode::Rich.error_indicator(), "✗");
assert_eq!(OutputMode::Plain.error_indicator(), "[ERROR]");
assert_eq!(OutputMode::Minimal.error_indicator(), "[ERROR]");
}
#[test]
fn test_warning_indicators() {
eprintln!(
"[TEST] warning_indicator: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.warning_indicator(),
OutputMode::Plain.warning_indicator(),
OutputMode::Minimal.warning_indicator()
);
assert_eq!(OutputMode::Rich.warning_indicator(), "âš ");
assert_eq!(OutputMode::Plain.warning_indicator(), "[WARN]");
assert_eq!(OutputMode::Minimal.warning_indicator(), "[WARN]");
}
#[test]
fn test_info_indicators() {
eprintln!(
"[TEST] info_indicator: Rich={}, Plain={}, Minimal={}",
OutputMode::Rich.info_indicator(),
OutputMode::Plain.info_indicator(),
OutputMode::Minimal.info_indicator()
);
assert_eq!(OutputMode::Rich.info_indicator(), "ℹ");
assert_eq!(OutputMode::Plain.info_indicator(), "[INFO]");
assert_eq!(OutputMode::Minimal.info_indicator(), "[INFO]");
}
#[test]
#[serial]
fn test_auto_explicit_plain_override() {
with_clean_env(|| {
set_env("FASTAPI_OUTPUT_MODE", "plain");
let mode = OutputMode::auto();
eprintln!("[TEST] Explicit plain override: {mode:?}");
assert_eq!(mode, OutputMode::Plain);
});
}
#[test]
#[serial]
fn test_auto_explicit_minimal_override() {
with_clean_env(|| {
set_env("FASTAPI_OUTPUT_MODE", "minimal");
let mode = OutputMode::auto();
eprintln!("[TEST] Explicit minimal override: {mode:?}");
assert_eq!(mode, OutputMode::Minimal);
});
}
#[test]
#[serial]
fn test_auto_agent_detected() {
with_clean_env(|| {
set_env("CLAUDE_CODE", "1");
let mode = OutputMode::auto();
eprintln!("[TEST] Agent detected mode: {mode:?}");
assert_eq!(mode, OutputMode::Plain);
});
}
#[test]
#[serial]
fn test_auto_ci_detected() {
with_clean_env(|| {
set_env("CI", "true");
let mode = OutputMode::auto();
eprintln!("[TEST] CI detected mode: {mode:?}");
assert_eq!(mode, OutputMode::Plain);
});
}
#[test]
#[serial]
fn test_auto_no_color_detected() {
with_clean_env(|| {
set_env("NO_COLOR", "1");
let mode = OutputMode::auto();
eprintln!("[TEST] NO_COLOR detected mode: {mode:?}");
assert_eq!(mode, OutputMode::Plain);
});
}
#[test]
#[serial]
fn test_explicit_override_beats_detection() {
with_clean_env(|| {
set_env("CLAUDE_CODE", "1");
set_env("FASTAPI_OUTPUT_MODE", "minimal");
let mode = OutputMode::auto();
eprintln!("[TEST] Override beats detection: {mode:?}");
assert_eq!(mode, OutputMode::Minimal);
});
}
#[test]
#[serial]
fn test_auto_deterministic() {
with_clean_env(|| {
set_env("CI", "true");
let mode1 = OutputMode::auto();
let mode2 = OutputMode::auto();
let mode3 = OutputMode::auto();
eprintln!("[TEST] Deterministic: {mode1:?} == {mode2:?} == {mode3:?}");
assert_eq!(mode1, mode2);
assert_eq!(mode2, mode3);
});
}
#[test]
fn test_parse_error_display() {
let err = OutputModeParseError("foobar".to_string());
let msg = err.to_string();
eprintln!("[TEST] Parse error display: {msg}");
assert!(msg.contains("foobar"));
assert!(msg.contains("rich"));
assert!(msg.contains("plain"));
assert!(msg.contains("minimal"));
}
#[test]
fn test_parse_error_is_error() {
let err = OutputModeParseError("x".to_string());
let _: &dyn std::error::Error = &err;
eprintln!("[TEST] OutputModeParseError implements Error trait");
}
#[test]
fn test_as_str_rich() {
assert_eq!(OutputMode::Rich.as_str(), "rich");
}
#[test]
fn test_as_str_plain() {
assert_eq!(OutputMode::Plain.as_str(), "plain");
}
#[test]
fn test_as_str_minimal() {
assert_eq!(OutputMode::Minimal.as_str(), "minimal");
}
#[test]
fn test_as_str_matches_display() {
assert_eq!(OutputMode::Rich.as_str(), OutputMode::Rich.to_string());
assert_eq!(OutputMode::Plain.as_str(), OutputMode::Plain.to_string());
assert_eq!(
OutputMode::Minimal.as_str(),
OutputMode::Minimal.to_string()
);
}
#[test]
fn test_is_agent_friendly_plain() {
assert!(OutputMode::Plain.is_agent_friendly());
}
#[test]
fn test_is_agent_friendly_rich() {
assert!(!OutputMode::Rich.is_agent_friendly());
}
#[test]
fn test_is_agent_friendly_minimal() {
assert!(!OutputMode::Minimal.is_agent_friendly());
}
#[test]
fn test_is_agent_friendly_consistency() {
let modes = [OutputMode::Rich, OutputMode::Plain, OutputMode::Minimal];
let agent_friendly_count = modes.iter().filter(|m| m.is_agent_friendly()).count();
assert_eq!(
agent_friendly_count, 1,
"Only Plain should be agent-friendly"
);
}
}