use anyhow::Result;
use console::Style;
use humantime::FormattedDuration;
use std::{
fmt::{self, Display, Formatter},
sync::LazyLock,
time::Duration,
};
use crate::config;
use crate::formatters::{
color::{BOLD_GREEN, BOLD_PINK, BOLD_YELLOW, DIM, NORMAL, color},
get_response_formatter,
host_stats::CompactHostStats,
response::ResponseFormatter,
stats::{OutputStats, ResponseStats},
};
use super::StatsFormatter;
struct CompactResponseStats {
stats: ResponseStats,
mode: config::OutputMode,
}
impl Display for CompactResponseStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stats = &self.stats;
let issues = stats
.error_map
.iter()
.chain(stats.timeout_map.iter())
.count();
if issues > 0 {
let input = if issues == 1 { "input" } else { "inputs" };
color!(
f,
BOLD_PINK,
"Issues found in {issues} {input}. Find details below.\n\n",
)?;
}
let response_formatter = get_response_formatter(&self.mode);
for (source, responses) in
super::sort_stats_iter(stats.error_map.iter().chain(stats.timeout_map.iter()))
{
color!(f, BOLD_YELLOW, "[{}]:\n", source)?;
write_responses(f, &*response_formatter, responses)?;
write_suggestions(f, stats, source)?;
writeln!(f)?;
}
color!(f, NORMAL, "🔍 {} Total", stats.total)?;
color!(f, DIM, " (in {})", format_duration(stats.duration))?;
color!(f, NORMAL, " 🔗 {} Unique", stats.unique)?;
color!(f, BOLD_GREEN, " ✅ {} OK", stats.successful)?;
let total_errors = stats.errors;
let err_str = if total_errors == 1 { "Error" } else { "Errors" };
color!(f, BOLD_PINK, " 🚫 {} {}", total_errors, err_str)?;
write_if_any(stats.unknown, "❓", "Unknown", &BOLD_PINK, f)?;
write_if_any(stats.excludes, "👻", "Excluded", &BOLD_YELLOW, f)?;
write_if_any(stats.timeouts, "⏳", "Timeouts", &BOLD_YELLOW, f)?;
write_if_any(stats.unsupported, "⛔", "Unsupported", &BOLD_YELLOW, f)?;
write_if_any(stats.redirects, "🔀", "Redirects", &BOLD_YELLOW, f)?;
Ok(())
}
}
fn format_duration(d: Duration) -> FormattedDuration {
#[allow(clippy::cast_possible_truncation)]
let truncated = Duration::from_millis(d.as_millis() as u64);
humantime::format_duration(truncated)
}
fn write_responses(
f: &mut Formatter<'_>,
response_formatter: &dyn ResponseFormatter,
responses: Vec<&lychee_lib::ResponseBody>,
) -> Result<(), fmt::Error> {
for response in responses {
writeln!(f, "{}", response_formatter.format_response(response))?;
}
Ok(())
}
fn write_suggestions(
f: &mut Formatter<'_>,
stats: &ResponseStats,
source: &lychee_lib::InputSource,
) -> Result<(), fmt::Error> {
if let Some(suggestions) = stats.suggestion_map.get(source) {
let mut sorted_suggestions: Vec<_> = suggestions.iter().collect();
sorted_suggestions.sort_by(|a, b| {
let (a, b) = (a.to_string().to_lowercase(), b.to_string().to_lowercase());
numeric_sort::cmp(&a, &b)
});
writeln!(f, "\nℹ Suggestions")?;
for suggestion in sorted_suggestions {
writeln!(f, "{suggestion}")?;
}
}
Ok(())
}
fn write_if_any(
value: usize,
symbol: &str,
text: &str,
style: &LazyLock<Style>,
f: &mut fmt::Formatter<'_>,
) -> Result<(), fmt::Error> {
if value > 0 {
color!(f, style, " {} {} {}", symbol, value, text)?;
}
Ok(())
}
pub(crate) struct Compact {
mode: config::OutputMode,
}
impl Compact {
pub(crate) const fn new(mode: config::OutputMode) -> Self {
Self { mode }
}
}
impl StatsFormatter for Compact {
fn format(&self, stats: OutputStats) -> Result<String> {
let response_stats = CompactResponseStats {
stats: stats.response_stats,
mode: self.mode.clone(),
};
let host_stats = CompactHostStats {
host_stats: stats.host_stats,
};
Ok(format!("{response_stats}\n{host_stats}"))
}
}
#[cfg(test)]
mod tests {
use crate::config::OutputMode;
use crate::formatters::stats::{StatsFormatter, get_dummy_stats};
use regex::Regex;
use super::*;
#[test]
fn test_formatter() {
let formatter = Compact::new(OutputMode::Plain);
let result = formatter.format(get_dummy_stats()).unwrap();
let without_color_codes = Regex::new(r"\u{1b}\[[0-9;]*m")
.unwrap()
.replace_all(&result, "")
.to_string();
assert_eq!(
without_color_codes,
"Issues found in 2 inputs. Find details below.
[https://example.com/]:
[404] https://github.com/mre/idiomatic-rust-doesnt-exist-man (at 1:1) | 404 Not Found
[TIMEOUT] https://httpbin.org/delay/2 (at 1:1) | Request timed out
ℹ Suggestions
https://original.dev/ --> https://suggestion.dev/
🔍 2 Total (in 0s) 🔗 2 Unique ✅ 0 OK 🚫 1 Error ⏳ 1 Timeouts 🔀 1 Redirects
📊 Per-host Statistics
────────────────────────────────────────────────────────────
example.com │ 5 reqs │ 60.0% success │ N/A median │ 20.0% cached
"
);
}
}