mod json;
mod spinner;
pub use json::*;
pub use spinner::Spinner;
use owo_colors::OwoColorize;
use crate::core::doctor::{CheckResult, CheckStatus};
pub fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.0} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.0} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub struct Output {
verbose: u8,
quiet: bool,
no_color: bool,
json: bool,
}
impl Output {
pub fn new(verbose: u8, quiet: bool, no_color: bool, json: bool) -> Self {
let no_color = no_color || std::env::var("NO_COLOR").is_ok();
Self {
verbose,
quiet,
no_color,
json,
}
}
pub fn success(&self, msg: &str) {
if self.quiet || self.json {
return;
}
if self.no_color {
eprintln!("✓ {msg}");
} else {
eprintln!("{} {msg}", "✓".green());
}
}
pub fn error(&self, msg: &str) {
if self.json {
return;
}
if self.no_color {
eprintln!("✗ {msg}");
} else {
eprintln!("{} {msg}", "✗".red());
}
}
pub fn info(&self, msg: &str) {
if self.quiet || self.json {
return;
}
if self.no_color {
eprintln!("• {msg}");
} else {
eprintln!("{} {msg}", "•".blue());
}
}
pub fn warn(&self, msg: &str) {
if self.quiet || self.json {
return;
}
if self.no_color {
eprintln!("⚠ {msg}");
} else {
eprintln!("{} {msg}", "⚠".yellow());
}
}
pub fn debug(&self, msg: &str) {
if self.quiet || self.json || self.verbose == 0 {
return;
}
if self.no_color {
eprintln!(" {msg}");
} else {
eprintln!(" {}", msg.dimmed());
}
}
pub fn println(&self, msg: &str) {
if self.quiet {
return;
}
println!("{msg}");
}
pub fn is_json(&self) -> bool {
self.json
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
pub fn verbosity(&self) -> u8 {
self.verbose
}
pub fn use_color(&self) -> bool {
!self.no_color
}
}
use crate::error::ScoopError;
use serde::Serialize;
impl Output {
pub fn json_success<T: Serialize>(&self, command: &'static str, data: T) {
if !self.json {
return;
}
let response = JsonResponse::success(command, data);
println!(
"{}",
serde_json::to_string_pretty(&response).unwrap_or_default()
);
}
pub fn json_error(&self, command: &'static str, error: &ScoopError) {
if !self.json {
return;
}
let mut response = JsonErrorResponse::error(command, error.code(), error.to_string());
if let Some(suggestion) = error.suggestion() {
response = response.with_suggestion(suggestion);
}
eprintln!(
"{}",
serde_json::to_string_pretty(&response).unwrap_or_default()
);
}
}
impl Default for Output {
fn default() -> Self {
Self::new(0, false, false, false)
}
}
impl Output {
pub fn doctor_header(&self) {
if self.quiet || self.json {
return;
}
eprintln!();
eprintln!("Checking installation...");
eprintln!();
}
pub fn doctor_check(&self, result: &CheckResult) {
if self.json {
return;
}
if self.quiet && result.is_ok() {
return;
}
let (icon, color_fn): (&str, fn(&str) -> String) = match &result.status {
CheckStatus::Ok => ("✓", |s| s.green().to_string()),
CheckStatus::Warning(_) => ("⚠", |s| s.yellow().to_string()),
CheckStatus::Error(_) => ("✗", |s| s.red().to_string()),
};
let message = match &result.status {
CheckStatus::Ok => result.name.to_string(),
CheckStatus::Warning(msg) => format!("{}: {}", result.name, msg),
CheckStatus::Error(msg) => format!("{}: {}", result.name, msg),
};
if self.no_color {
eprintln!("{} {}", icon, message);
} else {
eprintln!("{} {}", color_fn(icon), message);
}
if self.verbose > 0 {
if let Some(details) = &result.details {
if self.no_color {
eprintln!(" {}", details);
} else {
eprintln!(" {}", details.dimmed());
}
}
}
if let Some(suggestion) = &result.suggestion {
if self.no_color {
eprintln!(" → {}", suggestion);
} else {
eprintln!(" {} {}", "→".cyan(), suggestion);
}
}
}
pub fn doctor_summary(&self, results: &[CheckResult]) {
if self.json {
return;
}
let errors = results.iter().filter(|r| r.is_error()).count();
let warnings = results.iter().filter(|r| r.is_warning()).count();
eprintln!();
eprintln!("──────────────────────────────────");
if errors == 0 && warnings == 0 {
if self.no_color {
eprintln!("All checks passed!");
} else {
eprintln!("{}", "All checks passed!".green());
}
} else {
let mut parts = Vec::new();
if errors > 0 {
parts.push(format!("{} error(s)", errors));
}
if warnings > 0 {
parts.push(format!("{} warning(s)", warnings));
}
let summary = format!("Found {}.", parts.join(" and "));
if self.no_color {
eprintln!("{}", summary);
} else {
eprintln!("{}", summary.yellow());
}
}
}
pub fn doctor_json(&self, results: &[CheckResult]) {
if !self.json {
return;
}
let json_results: Vec<serde_json::Value> = results
.iter()
.map(|r| {
let status = match &r.status {
CheckStatus::Ok => "ok",
CheckStatus::Warning(_) => "warning",
CheckStatus::Error(_) => "error",
};
let message = match &r.status {
CheckStatus::Ok => None,
CheckStatus::Warning(msg) => Some(msg.clone()),
CheckStatus::Error(msg) => Some(msg.clone()),
};
serde_json::json!({
"id": r.id,
"name": r.name,
"status": status,
"message": message,
"suggestion": r.suggestion,
"details": r.details,
})
})
.collect();
let errors = results.iter().filter(|r| r.is_error()).count();
let warnings = results.iter().filter(|r| r.is_warning()).count();
let ok = results.iter().filter(|r| r.is_ok()).count();
let output = serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"summary": {
"total": results.len(),
"ok": ok,
"warnings": warnings,
"errors": errors,
},
"checks": json_results,
});
println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
);
}
}
#[cfg(test)]
mod tests {
use super::*;
mod format_size_tests {
use super::*;
#[test]
fn test_bytes() {
assert_eq!(format_size(0), "0 B");
assert_eq!(format_size(512), "512 B");
assert_eq!(format_size(1023), "1023 B");
}
#[test]
fn test_kilobytes() {
assert_eq!(format_size(1024), "1 KB");
assert_eq!(format_size(2048), "2 KB");
assert_eq!(format_size(10240), "10 KB");
}
#[test]
fn test_megabytes() {
assert_eq!(format_size(1_048_576), "1 MB");
assert_eq!(format_size(10_485_760), "10 MB");
}
#[test]
fn test_gigabytes() {
assert_eq!(format_size(1_073_741_824), "1.0 GB");
assert_eq!(format_size(2_147_483_648), "2.0 GB");
}
#[test]
fn test_boundary_values() {
assert_eq!(format_size(1023), "1023 B");
assert_eq!(format_size(1024), "1 KB");
assert_eq!(format_size(1_048_575), "1024 KB");
assert_eq!(format_size(1_048_576), "1 MB");
assert_eq!(format_size(1_073_741_823), "1024 MB");
assert_eq!(format_size(1_073_741_824), "1.0 GB");
}
}
mod output_flag_tests {
use super::*;
#[test]
fn is_json_returns_correct_value() {
let json_output = Output::new(0, false, false, true);
let normal_output = Output::new(0, false, false, false);
assert!(json_output.is_json());
assert!(!normal_output.is_json());
}
#[test]
fn is_quiet_returns_correct_value() {
let quiet_output = Output::new(0, true, false, false);
let normal_output = Output::new(0, false, false, false);
assert!(quiet_output.is_quiet());
assert!(!normal_output.is_quiet());
}
#[test]
fn default_output_has_expected_flags() {
let output = Output::default();
assert!(!output.is_json());
assert!(!output.is_quiet());
assert!(output.use_color()); }
#[test]
fn output_handles_max_verbosity() {
let output = Output::new(u8::MAX, false, false, false);
assert_eq!(output.verbosity(), u8::MAX);
}
#[test]
fn output_handles_all_flags_enabled() {
let output = Output::new(0, true, true, true);
assert!(output.is_quiet());
assert!(!output.use_color()); assert!(output.is_json());
}
#[test]
fn output_verbosity_levels() {
let v0 = Output::new(0, false, false, false);
let v1 = Output::new(1, false, false, false);
let v2 = Output::new(2, false, false, false);
assert_eq!(v0.verbosity(), 0);
assert_eq!(v1.verbosity(), 1);
assert_eq!(v2.verbosity(), 2);
assert!(v0.verbosity() < v1.verbosity());
assert!(v1.verbosity() < v2.verbosity());
}
}
}