Skip to content

Commit

Permalink
Add response formatter; refactor stats formatter
Browse files Browse the repository at this point in the history
This adds support for formatting responses in different ways.
For now the options are
* `plain`: No color, basic formatting
* `color`: Color, indented formatting (default)
* `emoji`: Fancy mode with emoji icons

Fixes #546
Related to #271
  • Loading branch information
mre committed Apr 4, 2024
1 parent 13f4339 commit 2c6b271
Show file tree
Hide file tree
Showing 22 changed files with 422 additions and 234 deletions.
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,30 @@ Available as a command-line utility, a library and a [GitHub Action](https://git
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents

- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Arch Linux](#arch-linux)
- [macOS](#macos)
- [Docker](#docker)
- [NixOS](#nixos)
- [FreeBSD](#freebsd)
- [Scoop](#scoop)
- [Termux](#termux)
- [Pre-built binaries](#pre-built-binaries)
- [Cargo](#cargo)
- [Build dependencies](#build-dependencies)
- [Compile and install lychee](#compile-and-install-lychee)
- [Feature flags](#feature-flags)
- [Features](#features)
- [Commandline usage](#commandline-usage)
- [Docker Usage](#docker-usage)
- [Linux/macOS shell command](#linuxmacos-shell-command)
- [Windows PowerShell command](#windows-powershell-command)
- [GitHub Token](#github-token)
- [Commandline Parameters](#commandline-parameters)
- [Exit codes](#exit-codes)
- [Ignoring links](#ignoring-links)
- [Caching](#caching)
- [Library usage](#library-usage)
- [GitHub Action Usage](#github-action-usage)
- [Contributing to lychee](#contributing-to-lychee)
Expand Down Expand Up @@ -443,6 +464,11 @@ Options:
-o, --output <OUTPUT>
Output file of status report
--mode <MODE>
Set the output display mode. Determines how results are presented in the terminal (color, plain, emoji)
[default: color]
-f, --format <FORMAT>
Output format of final status report (compact, detailed, json, markdown)
Expand All @@ -458,8 +484,6 @@ Options:
Print help (see a summary with '-h')
-V, --version
Print version
```

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

use crate::archive::{Archive, Suggestion};
use crate::formatters::response::ResponseFormatter;
use crate::formatters::get_response_formatter;
use crate::formatters::response::ResponseBodyFormatter;
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 ResponseBodyFormatter>,
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 @@ -275,10 +284,11 @@ fn show_progress(
output: &mut dyn Write,
progress_bar: &Option<ProgressBar>,
response: &Response,
formatter: &Arc<Box<dyn ResponseFormatter>>,
formatter: &dyn ResponseBodyFormatter,
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 @@ -317,30 +327,26 @@ fn get_failed_urls(stats: &mut ResponseStats) -> Vec<(InputSource, Url)> {
#[cfg(test)]
mod tests {
use log::info;
use lychee_lib::{CacheStatus, InputSource, Uri};

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

use crate::formatters;
use crate::{formatters::get_response_formatter, options};

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::ResponseFormat::Plain);
show_progress(
&mut buf,
&None,
&response,
&formatter,
formatter.as_ref(),
&Verbosity::default(),
)
.unwrap();
Expand All @@ -352,19 +358,23 @@ 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::ResponseFormat::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");
}
}
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,
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
//! Defines the colors used in the output of the CLI.

use console::Style;
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)*) => {
Expand Down
55 changes: 55 additions & 0 deletions lychee-bin/src/formatters/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use log::Level;
use std::io::Write;

use crate::{
formatters::{self, response::TOTAL_RESPONSE_OUTPUT_WIDTH},
options::ResponseFormat,
verbosity::Verbosity,
};

/// Initialize the logging system with the given verbosity level
pub(crate) fn init_logging(verbose: &Verbosity, mode: &ResponseFormat) {
let mut builder = env_logger::Builder::new();

builder
.format_timestamp(None) // Disable timestamps
.format_module_path(false) // Disable module path to reduce clutter
.format_target(false) // Disable target
.filter_module("lychee", verbose.log_level_filter()) // Re-add module filtering
.filter_module("lychee_lib", verbose.log_level_filter()); // Re-add module filtering

// Format the log messages if the output is not plain
if !matches!(mode, ResponseFormat::Plain) {
builder.format(|buf, record| {
let level = record.level();

// Spaces added to align the log levels
let level_text = match level {
Level::Error => "ERROR",
Level::Warn => " WARN",
Level::Info => " INFO",
Level::Debug => "DEBUG",
Level::Trace => "TRACE",
};

// Calculate the effective padding. Ensure it's non-negative to avoid panic.
let padding = TOTAL_RESPONSE_OUTPUT_WIDTH.saturating_sub(level_text.len() + 2); // +2 for brackets

// Construct the log prefix with the log level.
let level_label = format!("[{level_text}]");
let color = match level {
Level::Error => &formatters::color::BOLD_PINK,
Level::Warn => &formatters::color::BOLD_YELLOW,
Level::Info | Level::Debug => &formatters::color::BLUE,
Level::Trace => &formatters::color::DIM,
};
let colored_level = color.apply_to(level_label);
let prefix = format!("{}{}", " ".repeat(padding), colored_level);

// Write formatted log message with aligned level and original log message.
writeln!(buf, "{} {}", prefix, record.args())
});
}

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::ResponseBodyFormatter, stats::StatsFormatter};
use crate::options::{ResponseFormat, 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()
pub(crate) fn get_stats_formatter(
format: &StatsFormat,
response_format: &ResponseFormat,
) -> Box<dyn StatsFormatter> {
match format {
StatsFormat::Compact => Box::new(stats::Compact::new(response_format.clone())),
StatsFormat::Detailed => Box::new(stats::Detailed::new(response_format.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(format: &ResponseFormat) -> Box<dyn ResponseBodyFormatter> {
if !supports_color() {
return Box::new(response::PlainFormatter);
}
match format {
ResponseFormat::Plain => Box::new(response::PlainFormatter),
ResponseFormat::Color => Box::new(response::ColorFormatter),
ResponseFormat::Emoji => Box::new(response::EmojiFormatter),
}
Box::new(response::Color::new())
}

0 comments on commit 2c6b271

Please sign in to comment.