feat: add conversions for anstyle (#1581)

https://crates.io/crates/anstyle makes it possible to define colors in
an interoperable way. This makes it possible for applications to easily
load colors from a variety of formats.

This is gated by the anstyle feature flag which is disabled by default.

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
This commit is contained in:
Josh McKinney
2024-12-24 12:03:14 -08:00
committed by GitHub
parent 1d2882636e
commit 1d28c89fe5
4 changed files with 346 additions and 6 deletions

14
Cargo.lock generated
View File

@@ -360,7 +360,7 @@ dependencies = [
"semver 1.0.23",
"serde",
"serde_json",
"thiserror 2.0.6",
"thiserror 2.0.8",
]
[[package]]
@@ -2443,6 +2443,7 @@ dependencies = [
name = "ratatui-core"
version = "0.1.0-alpha.0"
dependencies = [
"anstyle",
"bitflags 2.6.0",
"cassowary",
"compact_str",
@@ -2458,6 +2459,7 @@ dependencies = [
"serde",
"serde_json",
"strum",
"thiserror 2.0.8",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
@@ -3228,11 +3230,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.6"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a"
dependencies = [
"thiserror-impl 2.0.6",
"thiserror-impl 2.0.8",
]
[[package]]
@@ -3248,9 +3250,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.6"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -24,6 +24,9 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
## enables conversions to / from colors, modifiers, and styles in the ['anstyle'] crate
anstyle = ["dep:anstyle"]
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
@@ -40,6 +43,7 @@ scrolling-regions = []
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
[dependencies]
anstyle = { version = "1", optional = true }
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.8.0"
@@ -51,6 +55,7 @@ palette = { version = "0.7.6", optional = true }
paste = "1.0.2"
serde = { workspace = true, optional = true }
strum.workspace = true
thiserror = "2"
unicode-segmentation.workspace = true
unicode-truncate = "2"
unicode-width.workspace = true

View File

@@ -79,6 +79,8 @@ pub use color::{Color, ParseColorError};
use stylize::ColorDebugKind;
pub use stylize::{Styled, Stylize};
#[cfg(feature = "anstyle")]
mod anstyle;
mod color;
pub mod palette;
#[cfg(feature = "palette")]

View File

@@ -0,0 +1,331 @@
//! This module contains conversion functions for styles from the `anstyle` crate.
use anstyle::{Ansi256Color, AnsiColor, Effects, RgbColor};
use thiserror::Error;
use super::{Color, Modifier, Style};
/// Error type for converting between `anstyle` colors and `Color`
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TryFromColorError {
#[error("cannot convert Ratatui Color to an Ansi256Color as it is not an indexed color")]
Ansi256,
#[error("cannot convert Ratatui Color to AnsiColor as it is not a 4-bit color")]
Ansi,
#[error("cannot convert Ratatui Color to RgbColor as it is not an RGB color")]
RgbColor,
}
impl From<Ansi256Color> for Color {
fn from(color: Ansi256Color) -> Self {
Self::Indexed(color.index())
}
}
impl TryFrom<Color> for Ansi256Color {
type Error = TryFromColorError;
fn try_from(color: Color) -> Result<Self, Self::Error> {
match color {
Color::Indexed(index) => Ok(Self(index)),
_ => Err(TryFromColorError::Ansi256),
}
}
}
impl From<AnsiColor> for Color {
fn from(value: AnsiColor) -> Self {
match value {
AnsiColor::Black => Self::Black,
AnsiColor::Red => Self::Red,
AnsiColor::Green => Self::Green,
AnsiColor::Yellow => Self::Yellow,
AnsiColor::Blue => Self::Blue,
AnsiColor::Magenta => Self::Magenta,
AnsiColor::Cyan => Self::Cyan,
AnsiColor::White => Self::Gray,
AnsiColor::BrightBlack => Self::DarkGray,
AnsiColor::BrightRed => Self::LightRed,
AnsiColor::BrightGreen => Self::LightGreen,
AnsiColor::BrightYellow => Self::LightYellow,
AnsiColor::BrightBlue => Self::LightBlue,
AnsiColor::BrightMagenta => Self::LightMagenta,
AnsiColor::BrightCyan => Self::LightCyan,
AnsiColor::BrightWhite => Self::White,
}
}
}
impl TryFrom<Color> for AnsiColor {
type Error = TryFromColorError;
fn try_from(color: Color) -> Result<Self, Self::Error> {
match color {
Color::Black => Ok(Self::Black),
Color::Red => Ok(Self::Red),
Color::Green => Ok(Self::Green),
Color::Yellow => Ok(Self::Yellow),
Color::Blue => Ok(Self::Blue),
Color::Magenta => Ok(Self::Magenta),
Color::Cyan => Ok(Self::Cyan),
Color::Gray => Ok(Self::White),
Color::DarkGray => Ok(Self::BrightBlack),
Color::LightRed => Ok(Self::BrightRed),
Color::LightGreen => Ok(Self::BrightGreen),
Color::LightYellow => Ok(Self::BrightYellow),
Color::LightBlue => Ok(Self::BrightBlue),
Color::LightMagenta => Ok(Self::BrightMagenta),
Color::LightCyan => Ok(Self::BrightCyan),
Color::White => Ok(Self::BrightWhite),
_ => Err(TryFromColorError::Ansi),
}
}
}
impl From<RgbColor> for Color {
fn from(color: RgbColor) -> Self {
Self::Rgb(color.r(), color.g(), color.b())
}
}
impl TryFrom<Color> for RgbColor {
type Error = TryFromColorError;
fn try_from(color: Color) -> Result<Self, Self::Error> {
match color {
Color::Rgb(red, green, blue) => Ok(Self(red, green, blue)),
_ => Err(TryFromColorError::RgbColor),
}
}
}
impl From<anstyle::Color> for Color {
fn from(color: anstyle::Color) -> Self {
match color {
anstyle::Color::Ansi(ansi_color) => Self::from(ansi_color),
anstyle::Color::Ansi256(ansi256_color) => Self::from(ansi256_color),
anstyle::Color::Rgb(rgb_color) => Self::from(rgb_color),
}
}
}
impl From<Color> for anstyle::Color {
fn from(color: Color) -> Self {
match color {
Color::Rgb(_, _, _) => Self::Rgb(RgbColor::try_from(color).unwrap()),
Color::Indexed(_) => Self::Ansi256(Ansi256Color::try_from(color).unwrap()),
_ => Self::Ansi(AnsiColor::try_from(color).unwrap()),
}
}
}
impl From<Effects> for Modifier {
fn from(effect: Effects) -> Self {
let mut modifier = Self::empty();
if effect.contains(Effects::BOLD) {
modifier |= Self::BOLD;
}
if effect.contains(Effects::DIMMED) {
modifier |= Self::DIM;
}
if effect.contains(Effects::ITALIC) {
modifier |= Self::ITALIC;
}
if effect.contains(Effects::UNDERLINE)
|| effect.contains(Effects::DOUBLE_UNDERLINE)
|| effect.contains(Effects::CURLY_UNDERLINE)
|| effect.contains(Effects::DOTTED_UNDERLINE)
|| effect.contains(Effects::DASHED_UNDERLINE)
{
modifier |= Self::UNDERLINED;
}
if effect.contains(Effects::BLINK) {
modifier |= Self::SLOW_BLINK;
}
if effect.contains(Effects::INVERT) {
modifier |= Self::REVERSED;
}
if effect.contains(Effects::HIDDEN) {
modifier |= Self::HIDDEN;
}
if effect.contains(Effects::STRIKETHROUGH) {
modifier |= Self::CROSSED_OUT;
}
modifier
}
}
impl From<Modifier> for Effects {
fn from(modifier: Modifier) -> Self {
let mut effects = Self::new();
if modifier.contains(Modifier::BOLD) {
effects |= Self::BOLD;
}
if modifier.contains(Modifier::DIM) {
effects |= Self::DIMMED;
}
if modifier.contains(Modifier::ITALIC) {
effects |= Self::ITALIC;
}
if modifier.contains(Modifier::UNDERLINED) {
effects |= Self::UNDERLINE;
}
if modifier.contains(Modifier::SLOW_BLINK) || modifier.contains(Modifier::RAPID_BLINK) {
effects |= Self::BLINK;
}
if modifier.contains(Modifier::REVERSED) {
effects |= Self::INVERT;
}
if modifier.contains(Modifier::HIDDEN) {
effects |= Self::HIDDEN;
}
if modifier.contains(Modifier::CROSSED_OUT) {
effects |= Self::STRIKETHROUGH;
}
effects
}
}
impl From<anstyle::Style> for Style {
fn from(style: anstyle::Style) -> Self {
Self {
fg: style.get_fg_color().map(Color::from),
bg: style.get_bg_color().map(Color::from),
add_modifier: style.get_effects().into(),
..Default::default()
}
}
}
impl From<Style> for anstyle::Style {
fn from(style: Style) -> Self {
let mut anstyle_style = Self::new();
if let Some(fg) = style.fg {
let fg = anstyle::Color::from(fg);
anstyle_style = anstyle_style.fg_color(Some(fg));
}
if let Some(bg) = style.bg {
let bg = anstyle::Color::from(bg);
anstyle_style = anstyle_style.bg_color(Some(bg));
}
anstyle_style = anstyle_style.effects(style.add_modifier.into());
anstyle_style
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anstyle_to_color() {
let anstyle_color = Ansi256Color(42);
let color = Color::from(anstyle_color);
assert_eq!(color, Color::Indexed(42));
}
#[test]
fn color_to_ansi256color() {
let color = Color::Indexed(42);
let anstyle_color = Ansi256Color::try_from(color);
assert_eq!(anstyle_color, Ok(Ansi256Color(42)));
}
#[test]
fn color_to_ansi256color_error() {
let color = Color::Rgb(0, 0, 0);
let anstyle_color = Ansi256Color::try_from(color);
assert_eq!(anstyle_color, Err(TryFromColorError::Ansi256));
}
#[test]
fn ansi_color_to_color() {
let ansi_color = AnsiColor::Red;
let color = Color::from(ansi_color);
assert_eq!(color, Color::Red);
}
#[test]
fn color_to_ansicolor() {
let color = Color::Red;
let ansi_color = AnsiColor::try_from(color);
assert_eq!(ansi_color, Ok(AnsiColor::Red));
}
#[test]
fn color_to_ansicolor_error() {
let color = Color::Rgb(0, 0, 0);
let ansi_color = AnsiColor::try_from(color);
assert_eq!(ansi_color, Err(TryFromColorError::Ansi));
}
#[test]
fn rgb_color_to_color() {
let rgb_color = RgbColor(255, 0, 0);
let color = Color::from(rgb_color);
assert_eq!(color, Color::Rgb(255, 0, 0));
}
#[test]
fn color_to_rgbcolor() {
let color = Color::Rgb(255, 0, 0);
let rgb_color = RgbColor::try_from(color);
assert_eq!(rgb_color, Ok(RgbColor(255, 0, 0)));
}
#[test]
fn color_to_rgbcolor_error() {
let color = Color::Indexed(42);
let rgb_color = RgbColor::try_from(color);
assert_eq!(rgb_color, Err(TryFromColorError::RgbColor));
}
#[test]
fn effects_to_modifier() {
let effects = Effects::BOLD | Effects::ITALIC;
let modifier = Modifier::from(effects);
assert!(modifier.contains(Modifier::BOLD));
assert!(modifier.contains(Modifier::ITALIC));
}
#[test]
fn modifier_to_effects() {
let modifier = Modifier::BOLD | Modifier::ITALIC;
let effects = Effects::from(modifier);
assert!(effects.contains(Effects::BOLD));
assert!(effects.contains(Effects::ITALIC));
}
#[test]
fn anstyle_style_to_style() {
let anstyle_style = anstyle::Style::new()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Red)))
.bg_color(Some(anstyle::Color::Ansi(AnsiColor::Blue)))
.effects(Effects::BOLD | Effects::ITALIC);
let style = Style::from(anstyle_style);
assert_eq!(style.fg, Some(Color::Red));
assert_eq!(style.bg, Some(Color::Blue));
assert!(style.add_modifier.contains(Modifier::BOLD));
assert!(style.add_modifier.contains(Modifier::ITALIC));
}
#[test]
fn style_to_anstyle_style() {
let style = Style {
fg: Some(Color::Red),
bg: Some(Color::Blue),
add_modifier: Modifier::BOLD | Modifier::ITALIC,
..Default::default()
};
let anstyle_style = anstyle::Style::from(style);
assert_eq!(
anstyle_style.get_fg_color(),
Some(anstyle::Color::Ansi(AnsiColor::Red))
);
assert_eq!(
anstyle_style.get_bg_color(),
Some(anstyle::Color::Ansi(AnsiColor::Blue))
);
assert!(anstyle_style.get_effects().contains(Effects::BOLD));
assert!(anstyle_style.get_effects().contains(Effects::ITALIC));
}
}