use crate::cli::color;
use crate::core::error::SsError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum OutputFormat {
#[default]
Human,
Json,
#[value(name = "md")]
Md,
}
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub format: OutputFormat,
pub verbose: bool,
pub quiet: bool,
pub color: bool,
}
impl OutputConfig {
fn human_suppressed(&self) -> bool {
self.format == OutputFormat::Json || self.quiet
}
pub fn print_step(&self, message: &str) {
if self.human_suppressed() {
return;
}
eprintln!("{} {message}", color::checkmark(self.color));
}
pub fn print_substep(&self, message: &str) {
if self.human_suppressed() {
return;
}
eprintln!("{} {message}", color::bullet(self.color));
}
pub fn print_warn(&self, message: &str) {
if self.human_suppressed() {
return;
}
eprintln!("{} {message}", color::warn_glyph(self.color));
}
pub fn print_info(&self, message: &str) {
if self.human_suppressed() {
return;
}
eprintln!("{message}");
}
pub fn print_error(&self, error: &SsError) {
match self.format {
OutputFormat::Human | OutputFormat::Md => {
let prefix = color::red("Error:", self.color);
eprintln!(
"{} {prefix} {} ({})",
color::cross(self.color),
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_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_json(&self) -> bool {
self.format == OutputFormat::Json
}
pub fn is_md(&self) -> bool {
self.format == OutputFormat::Md
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
pub fn create_spinner(&self, message: &str) -> Option<indicatif::ProgressBar> {
if self.human_suppressed() {
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::*;
use crate::core::error::ERR_ITEM_NOT_FOUND;
fn cfg(format: OutputFormat, quiet: bool) -> OutputConfig {
OutputConfig {
format,
verbose: false,
quiet,
color: false,
}
}
#[test]
fn is_quiet_reflects_flag() {
assert!(cfg(OutputFormat::Human, true).is_quiet());
assert!(!cfg(OutputFormat::Human, false).is_quiet());
}
#[test]
fn spinner_none_in_json_mode() {
assert!(cfg(OutputFormat::Json, false).create_spinner("x").is_none());
}
#[test]
fn spinner_none_in_quiet_mode() {
assert!(cfg(OutputFormat::Human, true).create_spinner("x").is_none());
}
#[test]
fn print_json_does_not_panic() {
let value = serde_json::json!({"status": "ok"});
cfg(OutputFormat::Json, false).print_json(&value);
}
#[test]
fn print_error_no_panic_both_modes() {
let err = SsError::new(ERR_ITEM_NOT_FOUND, "nope").with_suggestion("try x");
cfg(OutputFormat::Human, false).print_error(&err);
cfg(OutputFormat::Json, false).print_error(&err);
}
#[test]
fn human_output_silent_in_json_or_quiet() {
cfg(OutputFormat::Json, false).print_step("x");
cfg(OutputFormat::Human, true).print_substep("x");
cfg(OutputFormat::Json, false).print_warn("x");
}
}