use compact_str::CompactString;
use ratatui::style::{Color, Modifier, Style};
use unicode_width::UnicodeWidthStr;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct EnhancedCell {
symbol: CompactString,
pub fg: SerializableColor,
pub bg: SerializableColor,
pub modifiers: SerializableModifier,
pub underline_color: Option<SerializableColor>,
pub last_modified_frame: u64,
pub skip: bool,
}
impl EnhancedCell {
pub fn new() -> Self {
Self {
symbol: CompactString::from(" "),
fg: SerializableColor::Reset,
bg: SerializableColor::Reset,
modifiers: SerializableModifier::empty(),
underline_color: None,
last_modified_frame: 0,
skip: false,
}
}
pub fn with_symbol(symbol: impl Into<CompactString>) -> Self {
Self {
symbol: symbol.into(),
..Self::new()
}
}
pub fn from_ratatui_cell(cell: &ratatui::buffer::Cell, frame: u64) -> Self {
let style = cell.style();
Self {
symbol: CompactString::from(cell.symbol()),
fg: style
.fg
.map(SerializableColor::from)
.unwrap_or(SerializableColor::Reset),
bg: style
.bg
.map(SerializableColor::from)
.unwrap_or(SerializableColor::Reset),
modifiers: SerializableModifier::from(style.add_modifier),
underline_color: style.underline_color.map(SerializableColor::from),
last_modified_frame: frame,
skip: cell.skip,
}
}
pub fn symbol(&self) -> &str {
&self.symbol
}
pub fn set_symbol(&mut self, symbol: impl Into<CompactString>) {
self.symbol = symbol.into();
}
pub fn set_char(&mut self, c: char) {
self.symbol.clear();
self.symbol.push(c);
}
pub fn symbol_width(&self) -> usize {
self.symbol.width()
}
pub fn set_style(&mut self, style: Style) {
if let Some(fg) = style.fg {
self.fg = SerializableColor::from(fg);
}
if let Some(bg) = style.bg {
self.bg = SerializableColor::from(bg);
}
self.modifiers = self
.modifiers
.union(SerializableModifier::from(style.add_modifier));
self.modifiers = self
.modifiers
.difference(SerializableModifier::from(style.sub_modifier));
if let Some(underline) = style.underline_color {
self.underline_color = Some(SerializableColor::from(underline));
}
}
pub fn style(&self) -> Style {
Style::new()
.fg(self.fg.into())
.bg(self.bg.into())
.add_modifier(self.modifiers.into())
}
pub fn reset(&mut self) {
self.symbol = CompactString::from(" ");
self.fg = SerializableColor::Reset;
self.bg = SerializableColor::Reset;
self.modifiers = SerializableModifier::empty();
self.underline_color = None;
self.skip = false;
}
pub fn is_empty(&self) -> bool {
self.symbol == " "
&& self.fg == SerializableColor::Reset
&& self.bg == SerializableColor::Reset
&& self.modifiers.is_empty()
}
}
impl Default for EnhancedCell {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serialization", serde(rename_all = "snake_case"))]
pub enum SerializableColor {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Rgb { r: u8, g: u8, b: u8 },
Indexed(u8),
}
impl From<Color> for SerializableColor {
fn from(color: Color) -> Self {
match color {
Color::Reset => SerializableColor::Reset,
Color::Black => SerializableColor::Black,
Color::Red => SerializableColor::Red,
Color::Green => SerializableColor::Green,
Color::Yellow => SerializableColor::Yellow,
Color::Blue => SerializableColor::Blue,
Color::Magenta => SerializableColor::Magenta,
Color::Cyan => SerializableColor::Cyan,
Color::Gray => SerializableColor::Gray,
Color::DarkGray => SerializableColor::DarkGray,
Color::LightRed => SerializableColor::LightRed,
Color::LightGreen => SerializableColor::LightGreen,
Color::LightYellow => SerializableColor::LightYellow,
Color::LightBlue => SerializableColor::LightBlue,
Color::LightMagenta => SerializableColor::LightMagenta,
Color::LightCyan => SerializableColor::LightCyan,
Color::White => SerializableColor::White,
Color::Rgb(r, g, b) => SerializableColor::Rgb { r, g, b },
Color::Indexed(i) => SerializableColor::Indexed(i),
}
}
}
impl From<SerializableColor> for Color {
fn from(color: SerializableColor) -> Self {
match color {
SerializableColor::Reset => Color::Reset,
SerializableColor::Black => Color::Black,
SerializableColor::Red => Color::Red,
SerializableColor::Green => Color::Green,
SerializableColor::Yellow => Color::Yellow,
SerializableColor::Blue => Color::Blue,
SerializableColor::Magenta => Color::Magenta,
SerializableColor::Cyan => Color::Cyan,
SerializableColor::Gray => Color::Gray,
SerializableColor::DarkGray => Color::DarkGray,
SerializableColor::LightRed => Color::LightRed,
SerializableColor::LightGreen => Color::LightGreen,
SerializableColor::LightYellow => Color::LightYellow,
SerializableColor::LightBlue => Color::LightBlue,
SerializableColor::LightMagenta => Color::LightMagenta,
SerializableColor::LightCyan => Color::LightCyan,
SerializableColor::White => Color::White,
SerializableColor::Rgb { r, g, b } => Color::Rgb(r, g, b),
SerializableColor::Indexed(i) => Color::Indexed(i),
}
}
}
impl SerializableColor {
pub fn to_ansi_fg(self) -> String {
match self {
SerializableColor::Reset => "\x1b[39m".to_string(),
SerializableColor::Black => "\x1b[30m".to_string(),
SerializableColor::Red => "\x1b[31m".to_string(),
SerializableColor::Green => "\x1b[32m".to_string(),
SerializableColor::Yellow => "\x1b[33m".to_string(),
SerializableColor::Blue => "\x1b[34m".to_string(),
SerializableColor::Magenta => "\x1b[35m".to_string(),
SerializableColor::Cyan => "\x1b[36m".to_string(),
SerializableColor::Gray => "\x1b[37m".to_string(),
SerializableColor::DarkGray => "\x1b[90m".to_string(),
SerializableColor::LightRed => "\x1b[91m".to_string(),
SerializableColor::LightGreen => "\x1b[92m".to_string(),
SerializableColor::LightYellow => "\x1b[93m".to_string(),
SerializableColor::LightBlue => "\x1b[94m".to_string(),
SerializableColor::LightMagenta => "\x1b[95m".to_string(),
SerializableColor::LightCyan => "\x1b[96m".to_string(),
SerializableColor::White => "\x1b[97m".to_string(),
SerializableColor::Rgb { r, g, b } => format!("\x1b[38;2;{};{};{}m", r, g, b),
SerializableColor::Indexed(i) => format!("\x1b[38;5;{}m", i),
}
}
pub fn to_ansi_bg(self) -> String {
match self {
SerializableColor::Reset => "\x1b[49m".to_string(),
SerializableColor::Black => "\x1b[40m".to_string(),
SerializableColor::Red => "\x1b[41m".to_string(),
SerializableColor::Green => "\x1b[42m".to_string(),
SerializableColor::Yellow => "\x1b[43m".to_string(),
SerializableColor::Blue => "\x1b[44m".to_string(),
SerializableColor::Magenta => "\x1b[45m".to_string(),
SerializableColor::Cyan => "\x1b[46m".to_string(),
SerializableColor::Gray => "\x1b[47m".to_string(),
SerializableColor::DarkGray => "\x1b[100m".to_string(),
SerializableColor::LightRed => "\x1b[101m".to_string(),
SerializableColor::LightGreen => "\x1b[102m".to_string(),
SerializableColor::LightYellow => "\x1b[103m".to_string(),
SerializableColor::LightBlue => "\x1b[104m".to_string(),
SerializableColor::LightMagenta => "\x1b[105m".to_string(),
SerializableColor::LightCyan => "\x1b[106m".to_string(),
SerializableColor::White => "\x1b[107m".to_string(),
SerializableColor::Rgb { r, g, b } => format!("\x1b[48;2;{};{};{}m", r, g, b),
SerializableColor::Indexed(i) => format!("\x1b[48;5;{}m", i),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct SerializableModifier {
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underlined: bool,
pub slow_blink: bool,
pub rapid_blink: bool,
pub reversed: bool,
pub hidden: bool,
pub crossed_out: bool,
}
impl SerializableModifier {
pub const fn empty() -> Self {
Self {
bold: false,
dim: false,
italic: false,
underlined: false,
slow_blink: false,
rapid_blink: false,
reversed: false,
hidden: false,
crossed_out: false,
}
}
pub fn is_empty(&self) -> bool {
!self.bold
&& !self.dim
&& !self.italic
&& !self.underlined
&& !self.slow_blink
&& !self.rapid_blink
&& !self.reversed
&& !self.hidden
&& !self.crossed_out
}
pub fn union(self, other: Self) -> Self {
Self {
bold: self.bold || other.bold,
dim: self.dim || other.dim,
italic: self.italic || other.italic,
underlined: self.underlined || other.underlined,
slow_blink: self.slow_blink || other.slow_blink,
rapid_blink: self.rapid_blink || other.rapid_blink,
reversed: self.reversed || other.reversed,
hidden: self.hidden || other.hidden,
crossed_out: self.crossed_out || other.crossed_out,
}
}
pub fn difference(self, other: Self) -> Self {
Self {
bold: self.bold && !other.bold,
dim: self.dim && !other.dim,
italic: self.italic && !other.italic,
underlined: self.underlined && !other.underlined,
slow_blink: self.slow_blink && !other.slow_blink,
rapid_blink: self.rapid_blink && !other.rapid_blink,
reversed: self.reversed && !other.reversed,
hidden: self.hidden && !other.hidden,
crossed_out: self.crossed_out && !other.crossed_out,
}
}
pub fn to_ansi(self) -> String {
let mut codes = Vec::new();
if self.bold {
codes.push("1");
}
if self.dim {
codes.push("2");
}
if self.italic {
codes.push("3");
}
if self.underlined {
codes.push("4");
}
if self.slow_blink {
codes.push("5");
}
if self.rapid_blink {
codes.push("6");
}
if self.reversed {
codes.push("7");
}
if self.hidden {
codes.push("8");
}
if self.crossed_out {
codes.push("9");
}
if codes.is_empty() {
String::new()
} else {
format!("\x1b[{}m", codes.join(";"))
}
}
}
impl From<Modifier> for SerializableModifier {
fn from(modifier: Modifier) -> Self {
Self {
bold: modifier.contains(Modifier::BOLD),
dim: modifier.contains(Modifier::DIM),
italic: modifier.contains(Modifier::ITALIC),
underlined: modifier.contains(Modifier::UNDERLINED),
slow_blink: modifier.contains(Modifier::SLOW_BLINK),
rapid_blink: modifier.contains(Modifier::RAPID_BLINK),
reversed: modifier.contains(Modifier::REVERSED),
hidden: modifier.contains(Modifier::HIDDEN),
crossed_out: modifier.contains(Modifier::CROSSED_OUT),
}
}
}
impl From<SerializableModifier> for Modifier {
fn from(modifier: SerializableModifier) -> Self {
let mut m = Modifier::empty();
if modifier.bold {
m |= Modifier::BOLD;
}
if modifier.dim {
m |= Modifier::DIM;
}
if modifier.italic {
m |= Modifier::ITALIC;
}
if modifier.underlined {
m |= Modifier::UNDERLINED;
}
if modifier.slow_blink {
m |= Modifier::SLOW_BLINK;
}
if modifier.rapid_blink {
m |= Modifier::RAPID_BLINK;
}
if modifier.reversed {
m |= Modifier::REVERSED;
}
if modifier.hidden {
m |= Modifier::HIDDEN;
}
if modifier.crossed_out {
m |= Modifier::CROSSED_OUT;
}
m
}
}
#[cfg(test)]
mod tests;