diff --git a/README.md b/README.md index 7c0cfab..506dc92 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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")); @@ -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, @@ -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 @@ -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")); @@ -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: @@ -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 diff --git a/examples/basic.rs b/examples/basic.rs index 6590edf..316560e 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -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)); diff --git a/src/color.rs b/src/color.rs index 078baf4..3848fcb 100644 --- a/src/color.rs +++ b/src/color.rs @@ -114,6 +114,7 @@ impl NamedColor { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum ColorSpec { Named(NamedColor), + Ansi256(u8), Rgb(u8, u8, u8), } @@ -121,6 +122,7 @@ 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), } } @@ -128,6 +130,7 @@ impl ColorSpec { 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), } } diff --git a/src/lib.rs b/src/lib.rs index 0b48012..d3c7a1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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")); //! @@ -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 @@ -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 diff --git a/src/style.rs b/src/style.rs index b5eb2f4..d0bb7d2 100644 --- a/src/style.rs +++ b/src/style.rs @@ -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)) @@ -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. @@ -544,6 +573,22 @@ impl 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) } diff --git a/src/tests.rs b/src/tests.rs index 0c9140c..9af4731 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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)] @@ -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" @@ -264,6 +294,10 @@ 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] @@ -271,6 +305,15 @@ 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] @@ -493,6 +536,7 @@ 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] @@ -500,6 +544,7 @@ 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] @@ -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);