use crate::cli::color;
use crate::error::OlError;
#[derive(Debug, Clone, PartialEq)]
pub enum OutputFormat {
Human,
Json,
}
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub format: OutputFormat,
pub verbose: bool,
pub debug: bool,
pub quiet: bool,
pub color: bool,
}
impl OutputConfig {
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_error(&self, error: &OlError) {
match self.format {
OutputFormat::Human => {
let prefix = color::red("Error:", self.color);
eprintln!("{prefix} {} ({})", error.message, error.code);
if error.suggestion.is_some() || error.docs_url.is_some() {
eprintln!();
if let Some(ref s) = error.suggestion {
eprintln!(" Suggestion: {s}");
}
if let Some(ref url) = error.docs_url {
eprintln!(" Docs: {url}");
}
}
}
OutputFormat::Json => {
let json = serde_json::json!({
"error": {
"code": error.code,
"message": error.message,
"suggestion": error.suggestion,
"docs_url": error.docs_url,
}
});
eprintln!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
}
}
}
pub fn print_info(&self, message: &str) {
if self.quiet || self.format == OutputFormat::Json {
return;
}
eprintln!("{message}");
}
pub fn print_json<T: serde::Serialize>(&self, value: &T) {
match serde_json::to_string_pretty(value) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Error: failed to serialize JSON output: {e}");
}
}
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
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,
verbose: false,
debug: false,
quiet: false,
color: false,
}
}
fn json_config() -> OutputConfig {
OutputConfig {
format: OutputFormat::Json,
verbose: false,
debug: false,
quiet: false,
color: false,
}
}
fn quiet_config() -> OutputConfig {
OutputConfig {
format: OutputFormat::Human,
verbose: false,
debug: false,
quiet: true,
color: false,
}
}
#[test]
fn test_is_quiet_true_when_quiet_flag() {
let cfg = quiet_config();
assert!(cfg.is_quiet());
}
#[test]
fn test_is_quiet_false_in_normal_mode() {
let cfg = human_config();
assert!(!cfg.is_quiet());
}
#[test]
fn test_print_json_writes_valid_json() {
let cfg = json_config();
let value = serde_json::json!({"status": "ok", "version": "0.0.0"});
cfg.print_json(&value);
}
#[test]
fn test_create_spinner_returns_none_in_json_mode() {
let cfg = json_config();
let spinner = cfg.create_spinner("doing work...");
assert!(spinner.is_none(), "Spinner should be None in JSON mode");
}
#[test]
fn test_create_spinner_returns_none_in_quiet_mode() {
let cfg = quiet_config();
let spinner = cfg.create_spinner("doing work...");
assert!(spinner.is_none(), "Spinner should be None in quiet mode");
}
#[test]
fn test_output_format_equality() {
assert_eq!(OutputFormat::Human, OutputFormat::Human);
assert_eq!(OutputFormat::Json, OutputFormat::Json);
assert_ne!(OutputFormat::Human, OutputFormat::Json);
}
#[test]
fn test_print_error_json_mode_no_panic() {
let cfg = json_config();
let err = OlError::new(crate::error::ERR_UNKNOWN_AGENT, "Unknown agent")
.with_suggestion("Use a known agent type")
.with_docs("https://docs.openlatch.ai/errors/OL-1001");
cfg.print_error(&err);
}
#[test]
fn test_print_error_human_mode_no_panic() {
let cfg = human_config();
let err = OlError::new(crate::error::ERR_PORT_IN_USE, "Port 7443 in use");
cfg.print_error(&err);
}
#[test]
fn test_print_step_json_mode_is_silent() {
let cfg = json_config();
cfg.print_step("Installing hooks");
}
}