Skip to content

Commit

Permalink
Create utils for colorful and stylish terminal output
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvkb committed Jun 30, 2023
1 parent c04f847 commit c203c73
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 1 deletion.
109 changes: 109 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ rust-version = "1.70.0"
name = "pls"

[dependencies]
colored = "2.0.0"
lazy_static = "1.4.0"
regex = { version = "1.8.4", default-features = false, features = ["std", "perf"] }
unicode-segmentation = "1.10.1"

[profile.release]
# Reference: https://github.com/johnthagen/min-sized-rust
Expand Down
21 changes: 21 additions & 0 deletions src/fmt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! This module contains code for working with markup strings.
//!
//! Markup strings are a more convenient way to represent ANSI-formatted text.
//! They use an HTML-like syntax instead of arcane escape sequences.
//!
//! For example, to render the string "Hello, World!" in bold, you would write
//! `<bold>Hello, World!</>`.
//!
//! The tag consists of space separated directives. See [`fmt`](format::fmt) for
//! a list of supported directives. Tags can be nested, with inner tags capable
//! of overwriting directives from outer tags.
//!
//! The public interface of the module consists of two functions:
//!
//! * [`len`]
//! * [`render`]
mod format;
mod markup;

pub use markup::{len, render};
140 changes: 140 additions & 0 deletions src/fmt/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use colored::{Color, ColoredString, Colorize};
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
static ref TRUE_COLOR: Regex = Regex::new(
r"(?x)(?-u)^
rgb\(
(?P<red>\d{1,3}),\s?
(?P<green>\d{1,3}),\s?
(?P<blue>\d{1,3})
\)
$",
)
.unwrap();
}

/// Format the given string using the given list of directives.
///
/// Each of these directives can be a style or a color.
///
/// Styles can be one of a predefined list, namely 'blink', 'bold', 'dimmed',
/// 'hidden', 'italic', 'reversed', 'strikethrough', 'underline', or 'clear'.
/// Using the 'clear' style removes all existing styles from the string.
///
/// Colors can be one of the following:
///
/// * 8 ANSI colors
/// * 8 bright versions of the ANSI colors (with the 'bright_' prefix)
/// * true RGB colors (using the 'rgb(<red>,<green>,<blue>)' notation)
///
/// These colors are applied to the foreground text by default, but can be
/// applied to the background instead (using the 'bg:' prefix).
///
/// For more information, refer to the documentation for the
/// [colored](https://docs.rs/colored) crate.
///
/// # Arguments
///
/// * `text` - the string to format according to the style directives
/// * `directives` - the formatting directives to apply to the string
pub fn fmt<S, T>(text: S, directives: &[T]) -> String
where
S: AsRef<str>,
T: AsRef<str>,
{
let mut string = ColoredString::from(text.as_ref());
for directive in directives {
string = apply_directive(string, directive.as_ref())
}
string.to_string()
}

/// Apply a single directive to a `ColoredString` instance, consuming it and
/// returning a new `ColoredString` instance with that directive applied.
fn apply_directive(string: ColoredString, directive: &str) -> ColoredString {
let is_bg = directive.starts_with("bg:");
let directive = directive.replace("bg:", "").replace("bright_", "bright ");

let string = match directive.as_str() {
"clear" => return string.clear(), // no style
"blink" => return string.blink(),
"bold" => return string.bold(),
"dimmed" => return string.dimmed(),
"hidden" => return string.hidden(),
"italic" => return string.italic(),
"reversed" => return string.reversed(),
"strikethrough" => return string.strikethrough(),
"underline" => return string.underline(),
_ => string,
};

let mut color: Option<Color> = None;
if let Some(caps) = TRUE_COLOR.captures(&directive) {
// RGB true colors
let channels: Vec<_> = vec!["red", "green", "blue"]
.into_iter()
.filter_map(|x| caps[x].parse::<u8>().ok())
.collect();
if channels.len() == 3 {
color = Some(Color::TrueColor {
r: channels[0],
g: channels[1],
b: channels[2],
});
}
} else {
// Named ANSI colors
color = directive.parse().ok()
}

match color {
Some(col) if is_bg => string.on_color(col),
Some(col) => string.color(col),
None => string,
}
}

/// You can see the comprehensive list of escape codes for
/// [ANSI colours on Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors).
#[cfg(test)]
mod tests {
use super::fmt;

macro_rules! make_test {
( $($name:ident: $styles:expr => $prefix:expr, $suffix:expr,)* ) => {
$(
#[test]
fn $name() {
colored::control::set_override(true); // needed when running tests in CLion
let text = fmt("Hello, World!", $styles);
assert_eq!(text, format!("{}{}{}", $prefix, "Hello, World!", $suffix));
}
)*
};
}

make_test!(
test_fmt_applies_ansi_code_for_single_style: &["bold"] => "\x1b[1m", "\x1b[0m",
test_fmt_applies_ansi_code_for_multiple_styles: &["bold", "italic"] => "\x1b[1;3m", "\x1b[0m",

test_fmt_handles_reversed_colors: &["reversed"] => "\x1b[7m", "\x1b[0m",

test_fmt_applies_regular_text_color: &["blue"] => "\x1b[34m", "\x1b[0m",
test_fmt_applies_bright_text_color: &["bright_blue"] => "\x1b[94m", "\x1b[0m",
test_fmt_ignores_invalid_text_color: &["invalid"] => "", "",

test_fmt_applies_rgb_text_color: &["rgb(77,77,77)"] => "\x1b[38;2;77;77;77m", "\x1b[0m",
test_fmt_ignores_out_of_bounds_rgb_text_color: &["rgb(256,256,256)"] => "", "",

test_fmt_applies_regular_background_color: &["bg:blue"] => "\x1b[44m", "\x1b[0m",
test_fmt_applies_bright_background_color: &["bg:bright_blue"] => "\x1b[104m", "\x1b[0m",
test_fmt_ignores_invalid_background_color: &["bg:invalid"] => "", "",

test_fmt_applies_rgb_background_color: &["bg:rgb(77,77,77)"] => "\x1b[48;2;77;77;77m", "\x1b[0m",
test_fmt_ignores_out_of_bounds_rgb_background_color: &["bg:rgb(256,256,256)"] => "", "",

test_fmt_handles_clear_directive: &["bold", "italic", "clear"] => "", "", // no prefix, no suffix
);
}
Loading

0 comments on commit c203c73

Please sign in to comment.