-
-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add response formatter; refactor stats formatter
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
Showing
21 changed files
with
394 additions
and
232 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
use log::Level; | ||
use std::io::Write; | ||
|
||
use crate::{formatters, 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 | ||
|
||
// Enable color unless the user has disabled it | ||
if !matches!(mode, ResponseFormat::Plain) { | ||
builder.format(|buf, record| { | ||
let level = record.level(); | ||
let level_text = match level { | ||
Level::Error => "ERROR", | ||
Level::Warn => " WARN", | ||
Level::Info => " INFO", | ||
Level::Debug => "DEBUG", | ||
Level::Trace => "TRACE", | ||
}; | ||
|
||
// Desired total width including brackets | ||
let numeric_padding: usize = 10; | ||
// Calculate the effective padding. Ensure it's non-negative to avoid panic. | ||
let effective_padding = numeric_padding.saturating_sub(level_text.len() + 2); // +2 for brackets | ||
|
||
// Construct the log prefix with the log level. | ||
// The spaces added before "WARN" and "INFO" are to visually align them with "ERROR", "DEBUG", and "TRACE" | ||
let level_label = format!("[{level_text}]"); | ||
let c = 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 = c.apply_to(level_label); | ||
|
||
let prefix = format!("{}{}", " ".repeat(effective_padding), colored_level); | ||
|
||
// Write formatted log message with aligned level and original log message. | ||
writeln!(buf, "{} {}", prefix, record.args()) | ||
}); | ||
} | ||
|
||
builder.init(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
use lychee_lib::{CacheStatus, ResponseBody, Status}; | ||
|
||
use super::color::{DIM, GREEN, NORMAL, PINK, YELLOW}; | ||
|
||
/// A trait for formatting a response body | ||
/// | ||
/// This trait is used to format a response body into a string. | ||
/// It can be implemented for different formatting styles such as | ||
/// colorized output or plain text. | ||
pub(crate) trait ResponseBodyFormatter: Send + Sync { | ||
fn format_response(&self, body: &ResponseBody) -> String; | ||
} | ||
|
||
/// A basic formatter that just returns the response body as a string | ||
/// without any color codes or other formatting. | ||
/// | ||
/// Under the hood, it calls the `Display` implementation of the `ResponseBody` | ||
/// type. | ||
/// | ||
/// This formatter is used when the user has requested raw output | ||
/// or when the terminal does not support color. | ||
pub(crate) struct PlainFormatter; | ||
|
||
impl ResponseBodyFormatter for PlainFormatter { | ||
fn format_response(&self, body: &ResponseBody) -> String { | ||
body.to_string() | ||
} | ||
} | ||
|
||
/// A colorized formatter for the response body | ||
/// | ||
/// This formatter is used when the terminal supports color and the user | ||
/// has not explicitly requested raw, uncolored output. | ||
pub(crate) struct ColorFormatter; | ||
|
||
impl ResponseBodyFormatter for ColorFormatter { | ||
fn format_response(&self, body: &ResponseBody) -> String { | ||
// Determine the color based on the status. | ||
let status_color = match body.status { | ||
Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)) => &GREEN, | ||
Status::Excluded | ||
| Status::Unsupported(_) | ||
| Status::Cached(CacheStatus::Excluded | CacheStatus::Unsupported) => &DIM, | ||
Status::Redirected(_) => &NORMAL, | ||
Status::UnknownStatusCode(_) | Status::Timeout(_) => &YELLOW, | ||
Status::Error(_) | Status::Cached(CacheStatus::Error(_)) => &PINK, | ||
}; | ||
|
||
let status_formatted = format_status(&body.status); | ||
|
||
let colored_status = status_color.apply_to(status_formatted); | ||
|
||
// Construct the output. | ||
format!("{} {}", colored_status, body.uri) | ||
} | ||
} | ||
|
||
/// Desired total width of formatted string for color formatter | ||
/// | ||
/// The longest string, which needs to be formatted, is currently `[Excluded]` | ||
/// which is 10 characters long (including brackets). | ||
/// | ||
/// Keep in sync with `Status::code_as_string`, which converts status codes to | ||
/// strings. | ||
const STATUS_CODE_PADDING: usize = 10; | ||
|
||
/// Format the status code or text for the color formatter. | ||
/// | ||
/// Numeric status codes are right-aligned. | ||
/// Textual statuses are left-aligned. | ||
/// Padding is taken into account. | ||
fn format_status(status: &Status) -> String { | ||
let status_code_or_text = status.code_as_string(); | ||
|
||
// Calculate the effective padding. Ensure it's non-negative to avoid panic. | ||
let padding = STATUS_CODE_PADDING.saturating_sub(status_code_or_text.len() + 2); // +2 for brackets | ||
|
||
format!( | ||
"{}[{:>width$}]", | ||
" ".repeat(padding), | ||
status_code_or_text, | ||
width = status_code_or_text.len() | ||
) | ||
} | ||
|
||
/// An emoji formatter for the response body | ||
/// | ||
/// This formatter replaces certain textual elements with emojis for a more | ||
/// visual output. | ||
pub(crate) struct EmojiFormatter; | ||
|
||
impl ResponseBodyFormatter for EmojiFormatter { | ||
fn format_response(&self, body: &ResponseBody) -> String { | ||
let emoji = match body.status { | ||
Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)) => "✅", | ||
Status::Excluded | ||
| Status::Unsupported(_) | ||
| Status::Cached(CacheStatus::Excluded | CacheStatus::Unsupported) => "🚫", | ||
Status::Redirected(_) => "↪️", | ||
Status::UnknownStatusCode(_) | Status::Timeout(_) => "⚠️", | ||
Status::Error(_) | Status::Cached(CacheStatus::Error(_)) => "❌", | ||
}; | ||
format!("{} {}", emoji, body.uri) | ||
} | ||
} |
Oops, something went wrong.