use crate::cli::color;
use crate::cli::ui;
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;
}
if ui::is_active() {
ui::detail(message);
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;
}
if ui::is_active() {
ui::detail(&format!(" {message}"));
return;
}
let dot = color::bullet(self.color);
eprintln!("{dot} {message}");
}
pub fn print_error(&self, error: &OlError) {
if ui::fail(error) {
return;
}
ui::mark_error_reported(error);
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;
}
if ui::is_active() {
ui::detail(message);
return;
}
eprintln!("{message}");
}
pub fn print_notice(&self, message: &str) {
if self.quiet || self.format == OutputFormat::Json {
return;
}
if ui::is_active() {
ui::note(message.trim());
return;
}
eprintln!("{message}");
}
pub fn print_note(&self, message: &str) {
if ui::is_active() {
ui::note(message.trim_start());
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;
}
if ui::is_active() {
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 _l = crate::cli::ui::INSTALL_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let cfg = json_config();
let err = OlError::new(crate::error::ERR_EVENT_TOO_LARGE, "Event body exceeds 1 MB")
.with_suggestion("Split the payload")
.with_docs("https://docs.openlatch.ai/errors/OL-1002");
cfg.print_error(&err);
}
#[test]
fn test_print_error_human_mode_no_panic() {
let _l = crate::cli::ui::INSTALL_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
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");
}
#[test]
fn test_helpers_route_through_an_installed_rail() {
use crate::cli::ui::{self, Caps, Mode, Rail};
let _l = ui::INSTALL_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (rail, screen) = Rail::buffer(Caps::new(Mode::Plain, false, false, 80), 6);
let guard = ui::install_rail(rail).expect("installs");
let cfg = human_config();
ui::stage("Starting OpenLatch", "Started OpenLatch");
cfg.print_step("Daemon started on port 7443"); cfg.print_substep("PID 42"); cfg.print_info("Events will be forwarded"); cfg.print_note(" Check `openlatch status` shortly"); assert!(
cfg.create_spinner("x").is_none(),
"the rail owns the spinner"
);
let err = OlError::new("OL-9910", "service stopped");
cfg.print_error(&err);
drop(guard);
assert!(crate::cli::ui::error_reported(&err));
assert_eq!(
screen.lock().unwrap().clone(),
vec![
"[1/6] Starting OpenLatch...",
" Check openlatch status shortly",
"[1/6] Starting OpenLatch - failed",
" service stopped (OL-9910)",
]
);
}
#[test]
fn test_print_error_marks_the_error_reported_without_a_rail() {
let _l = crate::cli::ui::INSTALL_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let err = OlError::new("OL-9911", "printed once");
assert!(!crate::cli::ui::error_reported(&err));
human_config().print_error(&err);
assert!(crate::cli::ui::error_reported(&err));
}
}