Skip to content

Commit 77804c6

Browse files
committed
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
1 parent 8c6eee9 commit 77804c6

22 files changed

+422
-234
lines changed

README.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,30 @@ Available as a command-line utility, a library and a [GitHub Action](https://git
2020
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
2121
## Table of Contents
2222

23+
- [Table of Contents](#table-of-contents)
2324
- [Installation](#installation)
25+
- [Arch Linux](#arch-linux)
26+
- [macOS](#macos)
27+
- [Docker](#docker)
28+
- [NixOS](#nixos)
29+
- [FreeBSD](#freebsd)
30+
- [Scoop](#scoop)
31+
- [Termux](#termux)
32+
- [Pre-built binaries](#pre-built-binaries)
33+
- [Cargo](#cargo)
34+
- [Build dependencies](#build-dependencies)
35+
- [Compile and install lychee](#compile-and-install-lychee)
36+
- [Feature flags](#feature-flags)
2437
- [Features](#features)
2538
- [Commandline usage](#commandline-usage)
39+
- [Docker Usage](#docker-usage)
40+
- [Linux/macOS shell command](#linuxmacos-shell-command)
41+
- [Windows PowerShell command](#windows-powershell-command)
42+
- [GitHub Token](#github-token)
43+
- [Commandline Parameters](#commandline-parameters)
44+
- [Exit codes](#exit-codes)
45+
- [Ignoring links](#ignoring-links)
46+
- [Caching](#caching)
2647
- [Library usage](#library-usage)
2748
- [GitHub Action Usage](#github-action-usage)
2849
- [Pre-commit Usage](#pre-commit-usage)
@@ -458,6 +479,11 @@ Options:
458479
-o, --output <OUTPUT>
459480
Output file of status report
460481
482+
--mode <MODE>
483+
Set the output display mode. Determines how results are presented in the terminal (color, plain, emoji)
484+
485+
[default: color]
486+
461487
-f, --format <FORMAT>
462488
Output format of final status report (compact, detailed, json, markdown)
463489
@@ -473,8 +499,6 @@ Options:
473499
Print help (see a summary with '-h')
474500
475501
-V, --version
476-
Print version
477-
478502
```
479503

480504
### Exit codes

lychee-bin/src/commands/check.rs

+36-26
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use lychee_lib::{InputSource, Result};
1515
use lychee_lib::{ResponseBody, Status};
1616

1717
use crate::archive::{Archive, Suggestion};
18-
use crate::formatters::response::ResponseFormatter;
18+
use crate::formatters::get_response_formatter;
19+
use crate::formatters::response::ResponseBodyFormatter;
1920
use crate::verbosity::Verbosity;
2021
use crate::{cache::Cache, stats::ResponseStats, ExitCode};
2122

@@ -62,11 +63,13 @@ where
6263
accept,
6364
));
6465

66+
let formatter = get_response_formatter(&params.cfg.mode);
67+
6568
let show_results_task = tokio::spawn(progress_bar_task(
6669
recv_resp,
6770
params.cfg.verbose,
6871
pb.clone(),
69-
Arc::new(params.formatter),
72+
formatter,
7073
stats,
7174
));
7275

@@ -178,11 +181,17 @@ async fn progress_bar_task(
178181
mut recv_resp: mpsc::Receiver<Response>,
179182
verbose: Verbosity,
180183
pb: Option<ProgressBar>,
181-
formatter: Arc<Box<dyn ResponseFormatter>>,
184+
formatter: Box<dyn ResponseBodyFormatter>,
182185
mut stats: ResponseStats,
183186
) -> Result<(Option<ProgressBar>, ResponseStats)> {
184187
while let Some(response) = recv_resp.recv().await {
185-
show_progress(&mut io::stderr(), &pb, &response, &formatter, &verbose)?;
188+
show_progress(
189+
&mut io::stderr(),
190+
&pb,
191+
&response,
192+
formatter.as_ref(),
193+
&verbose,
194+
)?;
186195
stats.add(response);
187196
}
188197
Ok((pb, stats))
@@ -289,10 +298,11 @@ fn show_progress(
289298
output: &mut dyn Write,
290299
progress_bar: &Option<ProgressBar>,
291300
response: &Response,
292-
formatter: &Arc<Box<dyn ResponseFormatter>>,
301+
formatter: &dyn ResponseBodyFormatter,
293302
verbose: &Verbosity,
294303
) -> Result<()> {
295-
let out = formatter.write_response(response)?;
304+
let out = formatter.format_response(response.body());
305+
296306
if let Some(pb) = progress_bar {
297307
pb.inc(1);
298308
pb.set_message(out.clone());
@@ -330,31 +340,27 @@ fn get_failed_urls(stats: &mut ResponseStats) -> Vec<(InputSource, Url)> {
330340

331341
#[cfg(test)]
332342
mod tests {
343+
use crate::formatters;
344+
use crate::{formatters::get_response_formatter, options};
333345
use log::info;
334-
335346
use lychee_lib::{CacheStatus, ClientBuilder, InputSource, ResponseBody, Uri};
336347

337-
use crate::formatters;
338-
339348
use super::*;
340349

341350
#[test]
342351
fn test_skip_cached_responses_in_progress_output() {
343352
let mut buf = Vec::new();
344-
let response = Response(
353+
let response = Response::new(
354+
Uri::try_from("http://127.0.0.1").unwrap(),
355+
Status::Cached(CacheStatus::Ok(200)),
345356
InputSource::Stdin,
346-
ResponseBody {
347-
uri: Uri::try_from("http://127.0.0.1").unwrap(),
348-
status: Status::Cached(CacheStatus::Ok(200)),
349-
},
350357
);
351-
let formatter: Arc<Box<dyn ResponseFormatter>> =
352-
Arc::new(Box::new(formatters::response::Raw::new()));
358+
let formatter = get_response_formatter(&options::ResponseFormat::Plain);
353359
show_progress(
354360
&mut buf,
355361
&None,
356362
&response,
357-
&formatter,
363+
formatter.as_ref(),
358364
&Verbosity::default(),
359365
)
360366
.unwrap();
@@ -366,20 +372,24 @@ mod tests {
366372
#[test]
367373
fn test_show_cached_responses_in_progress_debug_output() {
368374
let mut buf = Vec::new();
369-
let response = Response(
375+
let response = Response::new(
376+
Uri::try_from("http://127.0.0.1").unwrap(),
377+
Status::Cached(CacheStatus::Ok(200)),
370378
InputSource::Stdin,
371-
ResponseBody {
372-
uri: Uri::try_from("http://127.0.0.1").unwrap(),
373-
status: Status::Cached(CacheStatus::Ok(200)),
374-
},
375379
);
376-
let formatter: Arc<Box<dyn ResponseFormatter>> =
377-
Arc::new(Box::new(formatters::response::Raw::new()));
378-
show_progress(&mut buf, &None, &response, &formatter, &Verbosity::debug()).unwrap();
380+
let formatter = get_response_formatter(&options::ResponseFormat::Plain);
381+
show_progress(
382+
&mut buf,
383+
&None,
384+
&response,
385+
formatter.as_ref(),
386+
&Verbosity::debug(),
387+
)
388+
.unwrap();
379389

380390
assert!(!buf.is_empty());
381391
let buf = String::from_utf8_lossy(&buf);
382-
assert_eq!(buf, "[200] http://127.0.0.1/ | Cached: OK (cached)\n");
392+
assert_eq!(buf, "[200] http://127.0.0.1/ | Cached: OK (cached)\n");
383393
}
384394

385395
#[tokio::test]

lychee-bin/src/commands/mod.rs

-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ pub(crate) use dump::dump_inputs;
88
use std::sync::Arc;
99

1010
use crate::cache::Cache;
11-
use crate::formatters::response::ResponseFormatter;
1211
use crate::options::Config;
1312
use lychee_lib::Result;
1413
use lychee_lib::{Client, Request};
@@ -18,6 +17,5 @@ pub(crate) struct CommandParams<S: futures::Stream<Item = Result<Request>>> {
1817
pub(crate) client: Client,
1918
pub(crate) cache: Arc<Cache>,
2019
pub(crate) requests: S,
21-
pub(crate) formatter: Box<dyn ResponseFormatter>,
2220
pub(crate) cfg: Config,
2321
}

lychee-bin/src/color.rs lychee-bin/src/formatters/color.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
//! Defines the colors used in the output of the CLI.
2+
13
use console::Style;
24
use once_cell::sync::Lazy;
35

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

7-
pub(crate) static GREEN: Lazy<Style> = Lazy::new(|| Style::new().color256(82).bright());
9+
pub(crate) static GREEN: Lazy<Style> = Lazy::new(|| Style::new().color256(2).bold().bright());
810
pub(crate) static BOLD_GREEN: Lazy<Style> = Lazy::new(|| Style::new().color256(82).bold().bright());
911
pub(crate) static YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bright());
1012
pub(crate) static BOLD_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bold().bright());
1113
pub(crate) static PINK: Lazy<Style> = Lazy::new(|| Style::new().color256(197));
1214
pub(crate) static BOLD_PINK: Lazy<Style> = Lazy::new(|| Style::new().color256(197).bold());
1315

16+
// Used for debug log messages
17+
pub(crate) static BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue().bright());
18+
1419
// Write output using predefined colors
1520
macro_rules! color {
1621
($f:ident, $color:ident, $text:tt, $($tts:tt)*) => {

lychee-bin/src/formatters/log.rs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use log::Level;
2+
use std::io::Write;
3+
4+
use crate::{
5+
formatters::{self, response::TOTAL_RESPONSE_OUTPUT_WIDTH},
6+
options::ResponseFormat,
7+
verbosity::Verbosity,
8+
};
9+
10+
/// Initialize the logging system with the given verbosity level
11+
pub(crate) fn init_logging(verbose: &Verbosity, mode: &ResponseFormat) {
12+
let mut builder = env_logger::Builder::new();
13+
14+
builder
15+
.format_timestamp(None) // Disable timestamps
16+
.format_module_path(false) // Disable module path to reduce clutter
17+
.format_target(false) // Disable target
18+
.filter_module("lychee", verbose.log_level_filter()) // Re-add module filtering
19+
.filter_module("lychee_lib", verbose.log_level_filter()); // Re-add module filtering
20+
21+
// Format the log messages if the output is not plain
22+
if !matches!(mode, ResponseFormat::Plain) {
23+
builder.format(|buf, record| {
24+
let level = record.level();
25+
26+
// Spaces added to align the log levels
27+
let level_text = match level {
28+
Level::Error => "ERROR",
29+
Level::Warn => " WARN",
30+
Level::Info => " INFO",
31+
Level::Debug => "DEBUG",
32+
Level::Trace => "TRACE",
33+
};
34+
35+
// Calculate the effective padding. Ensure it's non-negative to avoid panic.
36+
let padding = TOTAL_RESPONSE_OUTPUT_WIDTH.saturating_sub(level_text.len() + 2); // +2 for brackets
37+
38+
// Construct the log prefix with the log level.
39+
let level_label = format!("[{level_text}]");
40+
let color = match level {
41+
Level::Error => &formatters::color::BOLD_PINK,
42+
Level::Warn => &formatters::color::BOLD_YELLOW,
43+
Level::Info | Level::Debug => &formatters::color::BLUE,
44+
Level::Trace => &formatters::color::DIM,
45+
};
46+
let colored_level = color.apply_to(level_label);
47+
let prefix = format!("{}{}", " ".repeat(padding), colored_level);
48+
49+
// Write formatted log message with aligned level and original log message.
50+
writeln!(buf, "{} {}", prefix, record.args())
51+
});
52+
}
53+
54+
builder.init();
55+
}

lychee-bin/src/formatters/mod.rs

+23-29
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,41 @@
1+
pub(crate) mod color;
12
pub(crate) mod duration;
3+
pub(crate) mod log;
24
pub(crate) mod response;
35
pub(crate) mod stats;
46

5-
use lychee_lib::{CacheStatus, ResponseBody, Status};
7+
use self::{response::ResponseBodyFormatter, stats::StatsFormatter};
8+
use crate::options::{ResponseFormat, StatsFormat};
69
use supports_color::Stream;
710

8-
use crate::{
9-
color::{DIM, GREEN, NORMAL, PINK, YELLOW},
10-
options::{self, Format},
11-
};
12-
13-
use self::response::ResponseFormatter;
14-
1511
/// Detects whether a terminal supports color, and gives details about that
1612
/// support. It takes into account the `NO_COLOR` environment variable.
1713
fn supports_color() -> bool {
1814
supports_color::on(Stream::Stdout).is_some()
1915
}
2016

21-
/// Color the response body for TTYs that support it
22-
pub(crate) fn color_response(body: &ResponseBody) -> String {
23-
if supports_color() {
24-
let out = match body.status {
25-
Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)) => GREEN.apply_to(body),
26-
Status::Excluded
27-
| Status::Unsupported(_)
28-
| Status::Cached(CacheStatus::Excluded | CacheStatus::Unsupported) => {
29-
DIM.apply_to(body)
30-
}
31-
Status::Redirected(_) => NORMAL.apply_to(body),
32-
Status::UnknownStatusCode(_) | Status::Timeout(_) => YELLOW.apply_to(body),
33-
Status::Error(_) | Status::Cached(CacheStatus::Error(_)) => PINK.apply_to(body),
34-
};
35-
out.to_string()
36-
} else {
37-
body.to_string()
17+
pub(crate) fn get_stats_formatter(
18+
format: &StatsFormat,
19+
response_format: &ResponseFormat,
20+
) -> Box<dyn StatsFormatter> {
21+
match format {
22+
StatsFormat::Compact => Box::new(stats::Compact::new(response_format.clone())),
23+
StatsFormat::Detailed => Box::new(stats::Detailed::new(response_format.clone())),
24+
StatsFormat::Json => Box::new(stats::Json::new()),
25+
StatsFormat::Markdown => Box::new(stats::Markdown::new()),
26+
StatsFormat::Raw => Box::new(stats::Raw::new()),
3827
}
3928
}
4029

4130
/// Create a response formatter based on the given format option
42-
pub(crate) fn get_formatter(format: &options::Format) -> Box<dyn ResponseFormatter> {
43-
if matches!(format, Format::Raw) || !supports_color() {
44-
return Box::new(response::Raw::new());
31+
///
32+
pub(crate) fn get_response_formatter(format: &ResponseFormat) -> Box<dyn ResponseBodyFormatter> {
33+
if !supports_color() {
34+
return Box::new(response::PlainFormatter);
35+
}
36+
match format {
37+
ResponseFormat::Plain => Box::new(response::PlainFormatter),
38+
ResponseFormat::Color => Box::new(response::ColorFormatter),
39+
ResponseFormat::Emoji => Box::new(response::EmojiFormatter),
4540
}
46-
Box::new(response::Color::new())
4741
}

0 commit comments

Comments
 (0)