use std::io::IsTerminal as _;
use std::io::Write as _;
use std::sync::OnceLock;
static INVOCATION_OUTPUT: OnceLock<InvocationOutput> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OutputProtocol {
Text,
Json,
Raw,
}
#[derive(Debug)]
pub struct InvocationOutput {
protocol: OutputProtocol,
quiet: bool,
verbose: bool,
color: bool,
stdout_terminal: bool,
stderr_terminal: bool,
}
impl InvocationOutput {
pub fn capture(quiet: bool, verbose: bool, json: bool) -> Self {
Self::capture_protocol(
quiet,
verbose,
if json {
OutputProtocol::Json
} else {
OutputProtocol::Text
},
)
}
#[doc(hidden)]
pub fn capture_protocol(quiet: bool, verbose: bool, protocol: OutputProtocol) -> Self {
let stdout_terminal = std::io::stdout().is_terminal();
let stderr_terminal = std::io::stderr().is_terminal();
let raw_or_json = protocol != OutputProtocol::Text;
Self {
protocol,
quiet: quiet || raw_or_json,
verbose: verbose && !raw_or_json,
color: !raw_or_json && stderr_terminal && std::env::var_os("NO_COLOR").is_none(),
stdout_terminal,
stderr_terminal,
}
}
pub const fn protocol(&self) -> OutputProtocol {
self.protocol
}
pub const fn stdout_is_terminal(&self) -> bool {
self.stdout_terminal
}
pub const fn stderr_is_terminal(&self) -> bool {
self.stderr_terminal
}
pub const fn verbose(&self) -> bool {
self.verbose
}
pub const fn color_enabled(&self) -> bool {
self.color
}
}
pub const MACHINE_OUTPUT_SCHEMA_VERSION: u32 = 1;
#[doc(hidden)]
pub fn init(output: InvocationOutput) {
INVOCATION_OUTPUT
.set(output)
.expect("invocation output must be initialized exactly once");
}
fn invocation() -> &'static InvocationOutput {
INVOCATION_OUTPUT.get_or_init(|| InvocationOutput::capture(false, false, false))
}
pub fn is_quiet() -> bool {
invocation().quiet
}
pub fn is_json_mode() -> bool {
invocation().protocol == OutputProtocol::Json
}
pub fn is_verbose() -> bool {
invocation().verbose()
}
pub fn color_enabled() -> bool {
invocation().color_enabled()
}
#[doc(hidden)]
pub fn write_stdout(arguments: std::fmt::Arguments<'_>, newline: bool) {
let mut stdout = std::io::stdout().lock();
let result = stdout
.write_fmt(arguments)
.and_then(|()| if newline { stdout.write_all(b"\n") } else { Ok(()) });
if let Err(error) = result {
if error.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0);
}
if !is_json_mode()
&& let Err(_stderr_error) = writeln!(std::io::stderr().lock(), "error: failed writing stdout: {error}")
{
}
std::process::exit(1);
}
}
pub fn machine_json_envelope(
command: &str,
mode: &str,
result: &str,
exit_code: i32,
payload: serde_json::Value,
) -> serde_json::Value {
let mut out = serde_json::Map::new();
out.insert(
"schema_version".to_string(),
serde_json::Value::Number(serde_json::Number::from(MACHINE_OUTPUT_SCHEMA_VERSION)),
);
out.insert("command".to_string(), serde_json::Value::String(command.to_string()));
out.insert("mode".to_string(), serde_json::Value::String(mode.to_string()));
out.insert("result".to_string(), serde_json::Value::String(result.to_string()));
out.insert(
"exit_code".to_string(),
serde_json::Value::Number(serde_json::Number::from(exit_code)),
);
match payload {
serde_json::Value::Object(map) => {
for (key, value) in map {
if !out.contains_key(&key) {
out.insert(key, value);
}
}
}
other => {
out.insert("payload".to_string(), other);
}
}
serde_json::Value::Object(out)
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
if !$crate::output::is_json_mode() {
eprintln!("error: {}", format_args!($($arg)*))
}
};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
if !$crate::output::is_json_mode() {
eprintln!("warning: {}", format_args!($($arg)*))
}
};
}
#[macro_export]
macro_rules! help {
($($arg:tt)*) => {
if !$crate::output::is_json_mode() {
eprintln!("help: {}", format_args!($($arg)*))
}
};
}
#[macro_export]
macro_rules! status {
($($arg:tt)*) => {
if !$crate::output::is_quiet() {
eprintln!($($arg)*)
}
};
}
#[macro_export]
macro_rules! note {
($($arg:tt)*) => {
if !$crate::output::is_quiet() {
eprintln!("note: {}", format_args!($($arg)*))
}
};
}
#[macro_export]
macro_rules! progress {
($($arg:tt)*) => {
$crate::status!($($arg)*)
};
}
#[macro_export]
macro_rules! verbose_progress {
($($arg:tt)*) => {
if $crate::output::is_verbose() {
$crate::status!($($arg)*)
}
};
}
#[cfg(test)]
mod tests {
use super::{InvocationOutput, OutputProtocol};
#[test]
fn raw_protocol_suppresses_advisory_output_without_becoming_json() {
let output = InvocationOutput::capture_protocol(false, true, OutputProtocol::Raw);
assert_eq!(output.protocol(), OutputProtocol::Raw);
assert!(output.quiet, "raw streams must suppress progress and advisory output");
assert!(!output.verbose(), "raw streams must not enable text detail");
assert!(!output.color_enabled(), "raw streams must remain byte-stable");
}
}