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:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
331
ratatui-core/src/style/anstyle.rs
Normal file
331
ratatui-core/src/style/anstyle.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user