Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add response formatter; refactor stats formatter #1398

Merged
merged 17 commits into from
Jun 14, 2024
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,10 +458,17 @@ Options:
-o, --output <OUTPUT>
Output file of status report

--mode <MODE>
Set the output display mode. Determines how results are presented in the terminal

[default: color]
[possible values: plain, color, emoji]

-f, --format <FORMAT>
Output format of final status report (compact, detailed, json, markdown)
Output format of final status report

[default: compact]
[possible values: compact, detailed, json, markdown, raw]

--require-https
When HTTPS is available, treat HTTP links as errors
Expand All @@ -474,7 +481,6 @@ Options:

-V, --version
Print version

```

### Exit codes
Expand Down
61 changes: 35 additions & 26 deletions lychee-bin/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use lychee_lib::{InputSource, Result};
use lychee_lib::{ResponseBody, Status};

use crate::archive::{Archive, Suggestion};
use crate::formatters::get_response_formatter;
use crate::formatters::response::ResponseFormatter;
use crate::verbosity::Verbosity;
use crate::{cache::Cache, stats::ResponseStats, ExitCode};
Expand Down Expand Up @@ -62,11 +63,13 @@ where
accept,
));

let formatter = get_response_formatter(&params.cfg.mode);

let show_results_task = tokio::spawn(progress_bar_task(
recv_resp,
params.cfg.verbose,
pb.clone(),
Arc::new(params.formatter),
formatter,
stats,
));

Expand Down Expand Up @@ -178,11 +181,17 @@ async fn progress_bar_task(
mut recv_resp: mpsc::Receiver<Response>,
verbose: Verbosity,
pb: Option<ProgressBar>,
formatter: Arc<Box<dyn ResponseFormatter>>,
formatter: Box<dyn ResponseFormatter>,
mut stats: ResponseStats,
) -> Result<(Option<ProgressBar>, ResponseStats)> {
while let Some(response) = recv_resp.recv().await {
show_progress(&mut io::stderr(), &pb, &response, &formatter, &verbose)?;
show_progress(
&mut io::stderr(),
&pb,
&response,
formatter.as_ref(),
&verbose,
)?;
stats.add(response);
}
Ok((pb, stats))
Expand Down Expand Up @@ -289,10 +298,11 @@ fn show_progress(
output: &mut dyn Write,
progress_bar: &Option<ProgressBar>,
response: &Response,
formatter: &Arc<Box<dyn ResponseFormatter>>,
formatter: &dyn ResponseFormatter,
verbose: &Verbosity,
) -> Result<()> {
let out = formatter.write_response(response)?;
let out = formatter.format_response(response.body());

if let Some(pb) = progress_bar {
pb.inc(1);
pb.set_message(out.clone());
Expand Down Expand Up @@ -330,31 +340,26 @@ fn get_failed_urls(stats: &mut ResponseStats) -> Vec<(InputSource, Url)> {

#[cfg(test)]
mod tests {
use crate::{formatters::get_response_formatter, options};
use log::info;

use lychee_lib::{CacheStatus, ClientBuilder, InputSource, ResponseBody, Uri};

use crate::formatters;
use lychee_lib::{CacheStatus, ClientBuilder, InputSource, Uri};

use super::*;

#[test]
fn test_skip_cached_responses_in_progress_output() {
let mut buf = Vec::new();
let response = Response(
let response = Response::new(
Uri::try_from("http://127.0.0.1").unwrap(),
Status::Cached(CacheStatus::Ok(200)),
InputSource::Stdin,
ResponseBody {
uri: Uri::try_from("http://127.0.0.1").unwrap(),
status: Status::Cached(CacheStatus::Ok(200)),
},
);
let formatter: Arc<Box<dyn ResponseFormatter>> =
Arc::new(Box::new(formatters::response::Raw::new()));
let formatter = get_response_formatter(&options::OutputMode::Plain);
show_progress(
&mut buf,
&None,
&response,
&formatter,
formatter.as_ref(),
&Verbosity::default(),
)
.unwrap();
Expand All @@ -366,20 +371,24 @@ mod tests {
#[test]
fn test_show_cached_responses_in_progress_debug_output() {
let mut buf = Vec::new();
let response = Response(
let response = Response::new(
Uri::try_from("http://127.0.0.1").unwrap(),
Status::Cached(CacheStatus::Ok(200)),
InputSource::Stdin,
ResponseBody {
uri: Uri::try_from("http://127.0.0.1").unwrap(),
status: Status::Cached(CacheStatus::Ok(200)),
},
);
let formatter: Arc<Box<dyn ResponseFormatter>> =
Arc::new(Box::new(formatters::response::Raw::new()));
show_progress(&mut buf, &None, &response, &formatter, &Verbosity::debug()).unwrap();
let formatter = get_response_formatter(&options::OutputMode::Plain);
show_progress(
&mut buf,
&None,
&response,
formatter.as_ref(),
&Verbosity::debug(),
)
.unwrap();

assert!(!buf.is_empty());
let buf = String::from_utf8_lossy(&buf);
assert_eq!(buf, "[200] http://127.0.0.1/ | Cached: OK (cached)\n");
assert_eq!(buf, "[200] http://127.0.0.1/ | Cached: OK (cached)\n");
}

#[tokio::test]
Expand Down
2 changes: 0 additions & 2 deletions lychee-bin/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pub(crate) use dump::dump_inputs;
use std::sync::Arc;

use crate::cache::Cache;
use crate::formatters::response::ResponseFormatter;
use crate::options::Config;
use lychee_lib::Result;
use lychee_lib::{Client, Request};
Expand All @@ -18,6 +17,5 @@ pub(crate) struct CommandParams<S: futures::Stream<Item = Result<Request>>> {
pub(crate) client: Client,
pub(crate) cache: Arc<Cache>,
pub(crate) requests: S,
pub(crate) formatter: Box<dyn ResponseFormatter>,
pub(crate) cfg: Config,
}
18 changes: 17 additions & 1 deletion lychee-bin/src/color.rs → lychee-bin/src/formatters/color.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
//! Defines the colors used in the output of the CLI.

use console::Style;
use log::Level;
use once_cell::sync::Lazy;

pub(crate) static NORMAL: Lazy<Style> = Lazy::new(Style::new);
pub(crate) static DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());

pub(crate) static GREEN: Lazy<Style> = Lazy::new(|| Style::new().color256(82).bright());
pub(crate) static GREEN: Lazy<Style> = Lazy::new(|| Style::new().color256(2).bold().bright());
pub(crate) static BOLD_GREEN: Lazy<Style> = Lazy::new(|| Style::new().color256(82).bold().bright());
pub(crate) static YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bright());
pub(crate) static BOLD_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bold().bright());
pub(crate) static PINK: Lazy<Style> = Lazy::new(|| Style::new().color256(197));
pub(crate) static BOLD_PINK: Lazy<Style> = Lazy::new(|| Style::new().color256(197).bold());

// Used for debug log messages
pub(crate) static BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue().bright());

// Write output using predefined colors
macro_rules! color {
($f:ident, $color:ident, $text:tt, $($tts:tt)*) => {
write!($f, "{}", $color.apply_to(format!($text, $($tts)*)))
};
}

/// Returns the appropriate color for a given log level.
pub(crate) fn color_for_level(level: Level) -> &'static Style {
match level {
Level::Error => &BOLD_PINK,
Level::Warn => &BOLD_YELLOW,
Level::Info | Level::Debug => &BLUE,
Level::Trace => &DIM,
}
}

pub(crate) use color;
83 changes: 83 additions & 0 deletions lychee-bin/src/formatters/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use env_logger::{Builder, Env};
use log::LevelFilter;
use std::io::Write;

use crate::{
formatters::{self, response::MAX_RESPONSE_OUTPUT_WIDTH},
options::OutputMode,
verbosity::Verbosity,
};

/// Initialize the logging system with the given verbosity level.
pub(crate) fn init_logging(verbose: &Verbosity, mode: &OutputMode) {
// Set a base level for all modules to `warn`, which is a reasonable default.
// It will be overridden by RUST_LOG if it's set.
let env = Env::default().filter_or("RUST_LOG", "warn");

let mut builder = Builder::from_env(env);
builder
.format_timestamp(None)
.format_module_path(false)
.format_target(false);

if std::env::var("RUST_LOG").is_err() {
// Adjust the base log level filter based on the verbosity from CLI.
// This applies to all modules not explicitly mentioned in RUST_LOG.
let level_filter = verbose.log_level_filter();

// Apply a global filter. This ensures that, by default, other modules don't log at the debug level.
builder.filter_level(LevelFilter::Info);

// Apply more specific filters to your own crates, enabling more verbose logging as per `-vv`.
builder
.filter_module("lychee", level_filter)
.filter_module("lychee_lib", level_filter);
}

// Calculate the longest log level text, including brackets.
let max_level_text_width = log::LevelFilter::iter()
.map(|level| level.as_str().len() + 2)
.max()
.unwrap_or(0);

// Customize the log message format according to the output mode
if mode.is_plain() {
// Explicitly disable colors for plain output
builder.format(move |buf, record| writeln!(buf, "[{}] {}", record.level(), record.args()));
} else if mode.is_emoji() {
// Disable padding, keep colors
builder.format(move |buf, record| {
let level = record.level();
let color = formatters::color::color_for_level(level);
writeln!(
buf,
"{} {}",
color.apply_to(format!("[{level}]")),
record.args()
)
});
} else {
builder.format(move |buf, record| {
let level = record.level();
let level_text = format!("{level:5}");
let padding = (MAX_RESPONSE_OUTPUT_WIDTH.saturating_sub(max_level_text_width)).max(0);
let prefix = format!(
"{:<width$}",
format!("[{}]", level_text),
width = max_level_text_width
);
let color = formatters::color::color_for_level(level);
let colored_level = color.apply_to(&prefix);
writeln!(
buf,
"{:<padding$}{} {}",
"",
colored_level,
record.args(),
padding = padding
)
});
}

builder.init();
}
52 changes: 23 additions & 29 deletions lychee-bin/src/formatters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
pub(crate) mod color;
pub(crate) mod duration;
pub(crate) mod log;
pub(crate) mod response;
pub(crate) mod stats;

use lychee_lib::{CacheStatus, ResponseBody, Status};
use self::{response::ResponseFormatter, stats::StatsFormatter};
use crate::options::{OutputMode, StatsFormat};
use supports_color::Stream;

use crate::{
color::{DIM, GREEN, NORMAL, PINK, YELLOW},
options::{self, Format},
};

use self::response::ResponseFormatter;

/// Detects whether a terminal supports color, and gives details about that
/// support. It takes into account the `NO_COLOR` environment variable.
fn supports_color() -> bool {
supports_color::on(Stream::Stdout).is_some()
}

/// Color the response body for TTYs that support it
pub(crate) fn color_response(body: &ResponseBody) -> String {
if supports_color() {
let out = match body.status {
Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)) => GREEN.apply_to(body),
Status::Excluded
| Status::Unsupported(_)
| Status::Cached(CacheStatus::Excluded | CacheStatus::Unsupported) => {
DIM.apply_to(body)
}
Status::Redirected(_) => NORMAL.apply_to(body),
Status::UnknownStatusCode(_) | Status::Timeout(_) => YELLOW.apply_to(body),
Status::Error(_) | Status::Cached(CacheStatus::Error(_)) => PINK.apply_to(body),
};
out.to_string()
} else {
body.to_string()
/// Create a stats formatter based on the given format option
pub(crate) fn get_stats_formatter(
format: &StatsFormat,
mode: &OutputMode,
) -> Box<dyn StatsFormatter> {
match format {
StatsFormat::Compact => Box::new(stats::Compact::new(mode.clone())),
StatsFormat::Detailed => Box::new(stats::Detailed::new(mode.clone())),
StatsFormat::Json => Box::new(stats::Json::new()),
StatsFormat::Markdown => Box::new(stats::Markdown::new()),
StatsFormat::Raw => Box::new(stats::Raw::new()),
}
}

/// Create a response formatter based on the given format option
pub(crate) fn get_formatter(format: &options::Format) -> Box<dyn ResponseFormatter> {
if matches!(format, Format::Raw) || !supports_color() {
return Box::new(response::Raw::new());
pub(crate) fn get_response_formatter(mode: &OutputMode) -> Box<dyn ResponseFormatter> {
if !supports_color() {
return Box::new(response::PlainFormatter);
}
match mode {
OutputMode::Plain => Box::new(response::PlainFormatter),
OutputMode::Color => Box::new(response::ColorFormatter),
OutputMode::Emoji => Box::new(response::EmojiFormatter),
}
Box::new(response::Color::new())
}
Loading
Loading