use std::io::IsTerminal;
use std::sync::atomic::{AtomicBool, Ordering};
use serde_json::json;
use crate::error::{CliError, exit_code_for, kind_for};
pub fn use_color() -> bool {
!NO_COLOR.load(Ordering::Relaxed)
&& std::env::var_os("NO_COLOR").is_none()
&& std::io::stdout().is_terminal()
}
static NO_COLOR: AtomicBool = AtomicBool::new(false);
pub fn set_no_color(disabled: bool) {
NO_COLOR.store(disabled, Ordering::Relaxed);
}
pub fn terminal_width() -> usize {
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(80)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum OutputFormat {
Auto,
Text,
Json,
}
#[derive(Clone, Copy, Debug)]
pub struct OutputConfig {
pub json: bool,
pub quiet: bool,
}
impl OutputConfig {
pub fn new(format: OutputFormat, quiet: bool) -> Self {
let json = match format {
OutputFormat::Json => true,
OutputFormat::Text => false,
OutputFormat::Auto => !std::io::stdout().is_terminal(),
};
Self { json, quiet }
}
pub fn print_data(&self, data: &str) {
println!("{data}");
}
pub fn print_message(&self, msg: &str) {
if !self.quiet {
eprintln!("{msg}");
}
}
pub fn print_required_prompt(&self, msg: &str) {
eprintln!("{msg}");
}
pub fn print_json(&self, value: &serde_json::Value) {
println!(
"{}",
serde_json::to_string_pretty(value).expect("serialize JSON")
);
}
pub fn render_error(&self, err: &CliError) -> i32 {
let exit = exit_code_for(err);
let kind = kind_for(err);
let envelope = json!({
"error": {
"kind": kind,
"message": err.to_string(),
"exit_code": exit,
}
});
if self.json {
eprintln!(
"{}",
serde_json::to_string(&envelope).expect("serialize error envelope")
);
} else {
eprintln!("error: {err}");
}
exit
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_forced_on_when_not_tty() {
let cfg = OutputConfig::new(OutputFormat::Auto, false);
assert!(cfg.json);
}
#[test]
fn explicit_text_wins_over_auto() {
let cfg = OutputConfig::new(OutputFormat::Text, false);
assert!(!cfg.json, "text format must not emit JSON even when piped");
}
#[test]
fn explicit_json_wins_over_auto() {
let cfg = OutputConfig::new(OutputFormat::Json, false);
assert!(cfg.json);
}
#[test]
fn quiet_flag_propagates() {
let cfg = OutputConfig::new(OutputFormat::Auto, true);
assert!(cfg.quiet);
}
#[test]
fn render_error_returns_input_exit_for_input_error() {
let cfg = OutputConfig {
json: true,
quiet: true,
};
let exit = cfg.render_error(&CliError::Input("bad ref".into()));
assert_eq!(exit, 2);
}
#[test]
fn render_error_returns_auth_exit_for_auth_error() {
let cfg = OutputConfig {
json: true,
quiet: true,
};
let exit = cfg.render_error(&CliError::Auth("expired".into()));
assert_eq!(exit, 3);
}
#[test]
fn use_color_respects_no_color_env() {
unsafe { std::env::set_var("NO_COLOR", "1") };
assert!(!use_color());
unsafe { std::env::remove_var("NO_COLOR") };
}
}