Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Rust.
- Simple method-call syntax for applying colors and styles
- Support for basic colors, bright colors, and background colors
- Text styling (bold, dim, italic, underline, inverse, strikethrough)
- RGB and HEX color support for both text and background
- ANSI 256, RGB, and HEX color support for both text and background
- Composed style chaining with predictable override behavior
- Works with string literals, owned strings, and format macros
- Zero dependencies
Expand Down Expand Up @@ -53,7 +53,9 @@ println!("{}", "Bold text".bold());
println!("{}", "Italic text".italic());
println!("{}", "Underlined text".underline());

// RGB and Hex colors
// ANSI 256, RGB, and Hex colors
println!("{}", "ANSI 256 color".ansi256(208));
println!("{}", "ANSI 256 background".on_ansi256(236));
println!("{}", "Custom color".rgb(255, 128, 0));
println!("{}", "Custom background".on_rgb(0, 128, 255));
println!("{}", "Hex color".hex("#ff8000"));
Expand Down Expand Up @@ -127,8 +129,14 @@ println!("{}", "Back to plain text".red().bold().clear());
- `.inverse()` - Swap foreground and background colors
- `.strikethrough()` - Draw a line through the text

### RGB, HSL, and Hex Colors
### ANSI 256, RGB, HSL, and Hex Colors

- `.ansi256(index)` - Custom text color using an ANSI 256-color index (0-255,
compile-time enforced)
- `.on_ansi256(index)` - Custom background color using an ANSI 256-color index
(0-255, compile-time enforced)
- `.color256(index)` - Alias for `.ansi256(index)`
- `.on_color256(index)` - Alias for `.on_ansi256(index)`
- `.rgb(r, g, b)` - Custom text color using RGB values (0-255, compile-time
enforced)
- `.on_rgb(r, g, b)` - Custom background color using RGB values (0-255,
Expand All @@ -148,6 +156,8 @@ println!("{}", "Back to plain text".red().bold().clear());

- RGB values must be in range 0-255 (enforced at compile time via `u8` type)
- Attempting to use RGB values > 255 will result in a compile error
- ANSI 256-color indexes must be in range 0-255 (enforced at compile time via
`u8` type)
- Hex color codes can be provided with or without the '#' prefix in either
3-character shorthand or 6-character full form
- Invalid hex codes (wrong length, invalid characters) will result in plain
Expand All @@ -164,6 +174,11 @@ println!("{}", "Green".hsl(120.0, 100.0, 50.0)); // Pure green
println!("{}", "Blue".hsl(240.0, 100.0, 50.0)); // Pure blue
println!("{}", "Gray".hsl(0.0, 0.0, 50.0)); // 50% gray

// ANSI 256-color indexes use SGR 38;5/48;5 output
println!("{}", "Orange".ansi256(208));
println!("{}", "Dark background".on_ansi256(236));
println!("{}", "Alias".color256(208).on_color256(236));

// Hex colors work with or without #
println!("{}", "Hex color".hex("#ff8000"));
println!("{}", "Also valid".hex("ff8000"));
Expand Down Expand Up @@ -215,6 +230,10 @@ applications that want to force color on or off for a specific execution path.
`NO_COLOR` still takes precedence in `Auto` and `Always` mode. If `NO_COLOR` is
set, output is plain text.

ANSI 256-color methods use the same runtime policy as the named, RGB, HSL, and
hex color methods. `ColorMode` and `NO_COLOR` control whether ANSI 256 SGR
output is emitted.

For non-stdout destinations, use `StyledText::render` with a `RenderTarget` so
`Auto` mode evaluates the real output target:

Expand All @@ -238,6 +257,8 @@ your terminal emulator and its configuration:
- Basic colors (codes 30-37) are widely supported
- Bright colors (codes 90-97) may appear the same as basic colors in some
terminals or themes (pastel themes like Catppuccin especially)
- ANSI 256 colors use 256-color palette indexes with `38;5` and `48;5` SGR
sequences
- RGB colors require true color support in your terminal
- Some styling options (like italic) might not work in all terminals

Expand Down
7 changes: 5 additions & 2 deletions examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ fn main() {
println!("{}", "Inverse text".inverse());
println!("{}", "Strikethrough text".strikethrough());

// RGB, HSL, and Hex colors
println!("\nRGB, HSL, and Hex colors:");
// ANSI 256, RGB, HSL, and Hex colors
println!("\nANSI 256, RGB, HSL, and Hex colors:");
println!("{}", "ANSI 256 color".ansi256(208));
println!("{}", "ANSI 256 background".on_ansi256(236));
println!("{}", "ANSI 256 aliases".color256(208).on_color256(236));
println!("{}", "Custom RGB color".rgb(255, 128, 0));
println!("{}", "Custom RGB background".on_rgb(0, 128, 255));

Expand Down
3 changes: 3 additions & 0 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,23 @@ impl NamedColor {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ColorSpec {
Named(NamedColor),
Ansi256(u8),
Rgb(u8, u8, u8),
}

impl ColorSpec {
pub(crate) fn foreground_code(&self) -> String {
match self {
Self::Named(color) => color.foreground_code().to_string(),
Self::Ansi256(index) => format!("38;5;{index}"),
Self::Rgb(r, g, b) => format!("38;2;{};{};{}", r, g, b),
}
}

pub(crate) fn background_code(&self) -> String {
match self {
Self::Named(color) => color.background_code().to_string(),
Self::Ansi256(index) => format!("48;5;{index}"),
Self::Rgb(r, g, b) => format!("48;2;{};{};{}", r, g, b),
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
//! let name = "World";
//! println!("{}", format!("Hello, {}!", name.blue().bold()));
//!
//! // RGB and Hex colors
//! // ANSI 256, RGB, and Hex colors
//! println!("{}", "ANSI 256 color".ansi256(208));
//! println!("{}", "ANSI 256 background".on_ansi256(236));
//! println!("{}", "RGB color".rgb(255, 128, 0));
//! println!("{}", "Hex color".hex("#ff8000"));
//!
Expand All @@ -39,6 +41,7 @@
//! - Background colors
//! - Bright color variants
//! - Text styles (bold, dim, italic, underline)
//! - ANSI 256-color foreground and background support
//! - RGB, HSL, and Hex color support
//! - Composed style chaining
//! - Works with format! macro
Expand All @@ -48,6 +51,10 @@
//!
//! - RGB values must be in range 0-255 (enforced at compile time via `u8` type)
//! - Attempting to use RGB values > 255 will result in a compile error
//! - ANSI 256-color indexes must be in range 0-255 (enforced at compile time
//! via `u8` type)
//! - `.color256(index)` and `.on_color256(index)` are aliases for
//! `.ansi256(index)` and `.on_ansi256(index)`
//! - Hex color codes can be provided with or without the `#` prefix in 3-digit
//! shorthand or 6-digit full form
//! - Invalid hex codes (wrong length or invalid characters) return plain
Expand Down
45 changes: 45 additions & 0 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,26 @@ impl StyledText {
self.with_background(ColorSpec::Named(NamedColor::Black))
}

/// Apply an ANSI 256-color foreground.
pub fn ansi256(self, index: u8) -> Self {
self.with_foreground(ColorSpec::Ansi256(index))
}

/// Apply an ANSI 256-color background.
pub fn on_ansi256(self, index: u8) -> Self {
self.with_background(ColorSpec::Ansi256(index))
}

/// Alias for [`Self::ansi256`].
pub fn color256(self, index: u8) -> Self {
self.ansi256(index)
}

/// Alias for [`Self::on_ansi256`].
pub fn on_color256(self, index: u8) -> Self {
self.on_ansi256(index)
}

/// Apply a true-color RGB foreground.
pub fn rgb(self, r: u8, g: u8, b: u8) -> Self {
self.with_foreground(ColorSpec::Rgb(r, g, b))
Expand Down Expand Up @@ -407,6 +427,15 @@ pub trait Colorize {
/// Apply the standard black background color.
fn on_black(&self) -> StyledText;

/// Apply an ANSI 256-color foreground.
fn ansi256(&self, index: u8) -> StyledText;
/// Apply an ANSI 256-color background.
fn on_ansi256(&self, index: u8) -> StyledText;
/// Alias for [`Colorize::ansi256`].
fn color256(&self, index: u8) -> StyledText;
/// Alias for [`Colorize::on_ansi256`].
fn on_color256(&self, index: u8) -> StyledText;

/// Apply a true-color RGB foreground.
fn rgb(&self, r: u8, g: u8, b: u8) -> StyledText;
/// Apply a true-color RGB background.
Expand Down Expand Up @@ -544,6 +573,22 @@ impl<T: Display> Colorize for T {
StyledText::plain(self.to_string()).on_black()
}

fn ansi256(&self, index: u8) -> StyledText {
StyledText::plain(self.to_string()).ansi256(index)
}

fn on_ansi256(&self, index: u8) -> StyledText {
StyledText::plain(self.to_string()).on_ansi256(index)
}

fn color256(&self, index: u8) -> StyledText {
self.ansi256(index)
}

fn on_color256(&self, index: u8) -> StyledText {
self.on_ansi256(index)
}

fn rgb(&self, r: u8, g: u8, b: u8) -> StyledText {
StyledText::plain(self.to_string()).rgb(r, g, b)
}
Expand Down
51 changes: 51 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,35 @@ fn test_rgb_colors(#[case] r: u8, #[case] g: u8, #[case] b: u8) {
);
}

#[rstest]
#[case(0, "\x1b[38;5;0mtest\x1b[0m")]
#[case(255, "\x1b[38;5;255mtest\x1b[0m")]
fn test_ansi256_foreground_colors(#[case] index: u8, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".ansi256(index).to_string(), expected);
}

#[rstest]
#[case(0, "\x1b[48;5;0mtest\x1b[0m")]
#[case(255, "\x1b[48;5;255mtest\x1b[0m")]
fn test_ansi256_background_colors(#[case] index: u8, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".on_ansi256(index).to_string(), expected);
}

#[test]
fn test_color256_aliases_match_ansi256_methods() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!(
"test".color256(208).to_string(),
"test".ansi256(208).to_string()
);
assert_eq!(
"test".on_color256(236).to_string(),
"test".on_ansi256(236).to_string()
);
}

#[rstest]
#[case("#ff8000", 255, 128, 0)]
#[case("#f80", 255, 136, 0)]
Expand Down Expand Up @@ -246,6 +275,7 @@ fn test_clear_returns_plain_text() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".clear().to_string(), "test");
assert_eq!("test".red().clear().to_string(), "test");
assert_eq!("test".ansi256(208).clear().to_string(), "test");
assert_eq!(
"test".blue().italic().on_yellow().clear().to_string(),
"test"
Expand All @@ -264,13 +294,26 @@ fn test_chaining_composes_once() {
"test".rgb(255, 128, 0).on_blue().to_string(),
"\x1b[38;2;255;128;0;44mtest\x1b[0m"
);
assert_eq!(
"test".ansi256(208).bold().on_ansi256(236).to_string(),
"\x1b[1;38;5;208;48;5;236mtest\x1b[0m"
);
}

#[test]
fn test_conflicting_chains_use_last_color() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".red().green().to_string(), "\x1b[32mtest\x1b[0m");
assert_eq!("test".on_red().on_blue().to_string(), "\x1b[44mtest\x1b[0m");
assert_eq!(
"test".red().ansi256(208).to_string(),
"\x1b[38;5;208mtest\x1b[0m"
);
assert_eq!("test".ansi256(208).red().to_string(), "\x1b[31mtest\x1b[0m");
assert_eq!(
"test".on_blue().on_ansi256(236).to_string(),
"\x1b[48;5;236mtest\x1b[0m"
);
}

#[test]
Expand Down Expand Up @@ -493,13 +536,15 @@ fn test_color_mode_never_disables_color() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Never);
assert_eq!("test".red().to_string(), "test");
assert_eq!("test".blue().italic().on_yellow().to_string(), "test");
assert_eq!("test".ansi256(208).to_string(), "test");
}

#[test]
fn test_no_color_disables_output_in_auto_and_always() {
let _guard = TestStateGuard::no_color(ColorMode::Always);
assert_eq!("test".red().to_string(), "test");
assert_eq!("test".blue().italic().on_yellow().to_string(), "test");
assert_eq!("test".ansi256(208).to_string(), "test");
}

#[test]
Expand Down Expand Up @@ -535,6 +580,12 @@ fn test_bright_background_color_codes(#[case] color: NamedColor, #[case] expecte
assert_eq!(ColorSpec::Named(color).background_code(), expected);
}

#[test]
fn test_ansi256_color_codes() {
assert_eq!(ColorSpec::Ansi256(208).foreground_code(), "38;5;208");
assert_eq!(ColorSpec::Ansi256(236).background_code(), "48;5;236");
}

#[test]
fn test_from_styled_text_to_string() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
Expand Down
Loading