use crate::cli::{GlobalArgs, OutputFormat as CliOutputFormat};
use crate::error::OlError;
use crate::ui::{color, tty};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Human,
Json,
}
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub format: OutputFormat,
pub cli_format: CliOutputFormat,
pub verbose: bool,
pub debug: bool,
pub quiet: bool,
pub color: bool,
pub interactive: bool,
pub yes: bool,
pub dry_run: bool,
}
impl OutputConfig {
pub fn resolve(g: &GlobalArgs) -> Self {
let cli_format = g.output.unwrap_or_else(|| {
if tty::stdout_is_tty() && !tty::is_ci() {
CliOutputFormat::Table
} else {
CliOutputFormat::Json
}
});
let format = match cli_format {
CliOutputFormat::Table => OutputFormat::Human,
_ => OutputFormat::Json,
};
let color = match cli_format {
CliOutputFormat::Table => color::is_color_enabled(g.no_color),
_ => false,
};
Self {
format,
cli_format,
verbose: g.verbose,
debug: g.debug,
quiet: g.quiet,
color,
interactive: tty::allow_interactive(g.non_interactive),
yes: g.yes,
dry_run: g.dry_run,
}
}
pub fn is_human(&self) -> bool {
self.format == OutputFormat::Human && !self.quiet
}
pub fn is_machine(&self) -> bool {
self.format == OutputFormat::Json
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
pub fn print_step(&self, message: &str) {
if self.format == OutputFormat::Json || self.quiet {
return;
}
let mark = color::checkmark(self.color);
eprintln!("{mark} {message}");
}
pub fn print_substep(&self, message: &str) {
if self.format == OutputFormat::Json || self.quiet {
return;
}
let dot = color::bullet(self.color);
eprintln!("{dot} {message}");
}
pub fn print_info(&self, message: &str) {
if self.quiet || self.format == OutputFormat::Json {
return;
}
eprintln!("{message}");
}
pub fn print_error(&self, error: &OlError) {
match self.format {
OutputFormat::Human => {
let prefix = color::red("Error:", self.color);
eprintln!("{prefix} {} ({})", error.message, error.code.code);
if error.suggestion.is_some() {
eprintln!();
if let Some(ref s) = error.suggestion {
eprintln!(" Suggestion: {s}");
}
}
eprintln!(" Docs: {}", error.code.docs_url);
}
OutputFormat::Json => {
let json = serde_json::json!({
"error": {
"code": error.code.code,
"message": error.message,
"suggestion": error.suggestion,
"docs_url": error.code.docs_url,
"context": error.context,
}
});
eprintln!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
}
}
}
pub fn print_json<T: serde::Serialize>(&self, value: &T) {
if matches!(self.cli_format, CliOutputFormat::Yaml) {
match serde_yaml::to_string(value) {
Ok(s) => print!("{s}"),
Err(e) => eprintln!("Error: failed to serialize YAML output: {e}"),
}
return;
}
match serde_json::to_string_pretty(value) {
Ok(s) => println!("{s}"),
Err(e) => eprintln!("Error: failed to serialize JSON output: {e}"),
}
}
pub fn create_spinner(&self, message: &str) -> Option<indicatif::ProgressBar> {
if self.quiet || self.format == OutputFormat::Json {
return None;
}
let pb = indicatif::ProgressBar::new_spinner();
pb.set_draw_target(indicatif::ProgressDrawTarget::stderr());
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(80));
Some(pb)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn human_config() -> OutputConfig {
OutputConfig {
format: OutputFormat::Human,
cli_format: CliOutputFormat::Table,
verbose: false,
debug: false,
quiet: false,
color: false,
interactive: true,
yes: false,
dry_run: false,
}
}
fn json_config() -> OutputConfig {
OutputConfig {
format: OutputFormat::Json,
cli_format: CliOutputFormat::Json,
verbose: false,
debug: false,
quiet: false,
color: false,
interactive: false,
yes: false,
dry_run: false,
}
}
#[test]
fn explicit_json_overrides_tty_default() {
let g = GlobalArgs {
output: Some(CliOutputFormat::Json),
..Default::default()
};
let cfg = OutputConfig::resolve(&g);
assert_eq!(cfg.format, OutputFormat::Json);
assert!(cfg.is_machine());
}
#[test]
fn create_spinner_returns_none_in_json_mode() {
let cfg = json_config();
assert!(cfg.create_spinner("doing work").is_none());
}
#[test]
fn print_step_human_mode_does_not_panic() {
let cfg = human_config();
cfg.print_step("hello");
}
#[test]
fn print_error_json_mode_does_not_panic() {
let cfg = json_config();
let err = OlError::new(crate::error::OL_4220_HMAC_FAILED, "test").with_suggestion("retry");
cfg.print_error(&err);
}
}