use clap::ValueEnum;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lower")]
pub enum OutputMode {
Human,
Json,
Quiet,
Mcp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagInput {
Explicit(OutputMode),
JsonShort,
QuietShort,
None,
}
pub fn resolve(
forced_implicit: Option<OutputMode>,
flag: FlagInput,
env: Option<&str>,
is_tty: bool,
) -> OutputMode {
if let Some(m) = forced_implicit {
return m;
}
match flag {
FlagInput::Explicit(m) => m,
FlagInput::JsonShort => OutputMode::Json,
FlagInput::QuietShort => OutputMode::Quiet,
FlagInput::None => env.and_then(parse_env_mode).unwrap_or(if is_tty {
OutputMode::Human
} else {
OutputMode::Quiet
}),
}
}
pub fn parse_env_mode(s: &str) -> Option<OutputMode> {
match s.trim().to_ascii_lowercase().as_str() {
"human" => Some(OutputMode::Human),
"json" => Some(OutputMode::Json),
"quiet" => Some(OutputMode::Quiet),
"mcp" => Some(OutputMode::Mcp),
_ => None,
}
}
pub fn stdout_is_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forced_mcp_wins_over_flag_env_and_tty() {
let m = resolve(
Some(OutputMode::Mcp),
FlagInput::Explicit(OutputMode::Quiet),
Some("human"),
true,
);
assert_eq!(m, OutputMode::Mcp);
}
#[test]
fn explicit_flag_wins_over_env_and_tty() {
let m = resolve(
None,
FlagInput::Explicit(OutputMode::Json),
Some("human"),
true,
);
assert_eq!(m, OutputMode::Json);
}
#[test]
fn json_short_flag_implies_json() {
let m = resolve(None, FlagInput::JsonShort, Some("human"), true);
assert_eq!(m, OutputMode::Json);
}
#[test]
fn quiet_short_flag_implies_quiet() {
let m = resolve(None, FlagInput::QuietShort, Some("human"), true);
assert_eq!(m, OutputMode::Quiet);
}
#[test]
fn env_wins_when_no_flag() {
let m = resolve(None, FlagInput::None, Some("json"), true);
assert_eq!(m, OutputMode::Json);
}
#[test]
fn env_is_case_insensitive_and_trims_whitespace() {
assert_eq!(parse_env_mode("HUMAN"), Some(OutputMode::Human));
assert_eq!(parse_env_mode(" Json "), Some(OutputMode::Json));
assert_eq!(parse_env_mode("MCP"), Some(OutputMode::Mcp));
}
#[test]
fn unrecognised_env_falls_through_to_tty() {
let tty = resolve(None, FlagInput::None, Some("garbage"), true);
let pipe = resolve(None, FlagInput::None, Some("garbage"), false);
assert_eq!(tty, OutputMode::Human);
assert_eq!(pipe, OutputMode::Quiet);
}
#[test]
fn empty_env_falls_through_to_tty() {
assert_eq!(parse_env_mode(""), None);
assert_eq!(parse_env_mode(" "), None);
let tty = resolve(None, FlagInput::None, Some(""), true);
assert_eq!(tty, OutputMode::Human);
}
#[test]
fn tty_with_no_flag_no_env_yields_human() {
let m = resolve(None, FlagInput::None, None, true);
assert_eq!(m, OutputMode::Human);
}
#[test]
fn no_tty_with_no_flag_no_env_yields_quiet() {
let m = resolve(None, FlagInput::None, None, false);
assert_eq!(m, OutputMode::Quiet);
}
#[test]
fn env_does_not_override_explicit_flag() {
let m = resolve(
None,
FlagInput::Explicit(OutputMode::Quiet),
Some("human"),
true,
);
assert_eq!(m, OutputMode::Quiet);
}
#[test]
fn forced_implicit_overrides_env() {
let m = resolve(Some(OutputMode::Mcp), FlagInput::None, Some("human"), true);
assert_eq!(m, OutputMode::Mcp);
}
}