//! Cell styles, color resolution, and VT/HTML serialization. use crate::page::{Cell, ContentTag}; use ghostty_vt::{ color::{Palette, Rgb}, sgr::Underline, }; use std::{ fmt, hash::{Hash, Hasher}, }; /// Nonzero page-local style identifier. pub type Id = u16; /// The default style needs no set lookup. pub const DEFAULT_ID: Id = 0; /// A style color retains its palette or RGB origin. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum Color { /// Unspecified terminal default. #[default] None, /// Index in the current palette. Palette(u8), /// Literal RGB value. Rgb(Rgb), } impl Color { /// Compare source and value. pub fn eql(self, other: Self) -> bool { self == other } /// Resolve a palette-dependent color. pub fn resolve(self, palette: &Palette) -> Option { match self { Self::None => None, Self::Palette(i) => Some(palette[usize::from(i)]), Self::Rgb(v) => Some(v), } } } /// SGR boolean flags and underline kind. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Flags { /// SGR bold attribute. pub bold: bool, /// SGR italic attribute. pub italic: bool, /// SGR faint attribute. pub faint: bool, /// SGR blink attribute. pub blink: bool, /// SGR inverse attribute. pub inverse: bool, /// SGR invisible attribute. pub invisible: bool, /// SGR strikethrough attribute. pub strikethrough: bool, /// SGR overline attribute. pub overline: bool, /// Underline decoration kind. pub underline: Underline, } impl Default for Flags { fn default() -> Self { Self { bold: false, italic: false, faint: false, blink: false, inverse: false, invisible: false, strikethrough: false, overline: false, underline: Underline::None, } } } impl Hash for Flags { fn hash(&self, state: &mut H) { self.bold.hash(state); self.italic.hash(state); self.faint.hash(state); self.blink.hash(state); self.inverse.hash(state); self.invisible.hash(state); self.strikethrough.hash(state); self.overline.hash(state); (self.underline as u8).hash(state); } } /// Fully self-contained cell styling. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct Style { /// Foreground color. pub fg_color: Color, /// Background color. pub bg_color: Color, /// Underline color. pub underline_color: Color, /// SGR flags. pub flags: Flags, } /// Bold foreground policy, independent of the configuration crate. // TODO(integration): convert ghostty-config's BoldColor at the Screen boundary. #[derive(Debug, Clone, Copy)] pub enum BoldColor { /// Use bright palette colors for the first eight entries. Bright, /// Override the default foreground when bold. Color(Rgb), } /// Inputs needed to resolve foreground colors. #[derive(Clone, Copy)] pub struct Fg<'a> { /// Terminal default foreground. pub default: Rgb, /// Current palette. pub palette: &'a Palette, /// Optional bold override. pub bold: Option, } impl Style { /// Ghostty's packed-style hash: fold the exact 128-bit representation and /// apply Zig's 64-bit integer hash. This determines page-local probe order. pub(crate) fn zig_hash(&self) -> u64 { const MULTIPLIER: u64 = 0xbea225f9eb34556d; let mut packed = 0u128; for (index, color) in [self.fg_color, self.bg_color, self.underline_color] .into_iter() .enumerate() { let (tag, data) = match color { Color::None => (0, 0), Color::Palette(value) => (1, u32::from(value)), Color::Rgb(rgb) => ( 2, u32::from(rgb.r) | u32::from(rgb.g) << 8 | u32::from(rgb.b) << 16, ), }; packed |= (tag as u128) << (index * 8); packed |= u128::from(data) << (24 + index * 24); } let flags = self.flags; let mut bits = 0u16; for (index, value) in [ flags.bold, flags.italic, flags.faint, flags.blink, flags.inverse, flags.invisible, flags.strikethrough, flags.overline, ] .into_iter() .enumerate() { bits |= u16::from(value) << index; } bits |= (flags.underline as u16) << 8; packed |= u128::from(bits) << 96; let mut hash = packed as u64 ^ (packed >> 64) as u64; hash = (hash ^ (hash >> 32)).wrapping_mul(MULTIPLIER); hash = (hash ^ (hash >> 29)).wrapping_mul(MULTIPLIER); hash = (hash ^ (hash >> 32)).wrapping_mul(MULTIPLIER); hash ^ (hash >> 29) } /// True for default styling. pub fn is_default(self) -> bool { self == Self::default() } /// Compare all style fields. pub fn eql(self, other: Self) -> bool { self == other } /// Resolve background, including background-only cells. pub fn bg(self, cell: &Cell, palette: &Palette) -> Option { match cell.content_tag() { ContentTag::BgColorPalette => Some(palette[usize::from(cell.color_palette())]), ContentTag::BgColorRgb => Some(cell.color_rgb()), _ => self.bg_color.resolve(palette), } } /// Resolve foreground using bold palette and default overrides. pub fn fg(self, opts: Fg<'_>) -> Rgb { match self.fg_color { Color::None => { if self.flags.bold { match opts.bold { Some(BoldColor::Color(c)) => c, _ => opts.default, } } else { opts.default } } Color::Palette(i) => { opts.palette[usize::from(if self.flags.bold && opts.bold.is_some() && i < 8 { i + 8 } else { i })] } Color::Rgb(c) => { if self.flags.bold && c == opts.default { match opts.bold { Some(BoldColor::Color(v)) => v, _ => c, } } else { c } } } } /// Resolve underline color. pub fn underline_color(self, palette: &Palette) -> Option { self.underline_color.resolve(palette) } /// Make a background-only cell, if a background is specified. pub fn bg_cell(self) -> Option { let mut c = Cell::default(); match self.bg_color { Color::None => return None, Color::Palette(i) => { c.set_content_tag(ContentTag::BgColorPalette); c.set_color_palette(i); } Color::Rgb(v) => { c.set_content_tag(ContentTag::BgColorRgb); c.set_color_rgb(v); } } Some(c) } /// Render self-contained SGR sequences, starting with reset. pub fn formatter_vt(&self) -> VtFormatter<'_> { VtFormatter { style: self, palette: None, } } /// Render inline CSS declarations. pub fn formatter_html(&self) -> HtmlFormatter<'_> { HtmlFormatter { style: self, palette: None, } } } /// VT sequence formatter. pub struct VtFormatter<'a> { /// Style being formatted. pub style: &'a Style, /// Resolve palette entries to RGB when provided. pub palette: Option<&'a Palette>, } impl fmt::Display for VtFormatter<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[0m")?; if self.style.flags.bold { f.write_str("\x1b[1m")?; } if self.style.flags.faint { f.write_str("\x1b[2m")?; } if self.style.flags.italic { f.write_str("\x1b[3m")?; } if self.style.flags.blink { f.write_str("\x1b[5m")?; } if self.style.flags.inverse { f.write_str("\x1b[7m")?; } if self.style.flags.invisible { f.write_str("\x1b[8m")?; } if self.style.flags.strikethrough { f.write_str("\x1b[9m")?; } if self.style.flags.overline { f.write_str("\x1b[53m")?; } f.write_str(match self.style.flags.underline { Underline::None => "", Underline::Single => "\x1b[4m", Underline::Double => "\x1b[4:2m", Underline::Curly => "\x1b[4:3m", Underline::Dotted => "\x1b[4:4m", Underline::Dashed => "\x1b[4:5m", })?; for (prefix, mut c) in [ (38, self.style.fg_color), (48, self.style.bg_color), (58, self.style.underline_color), ] { if let (Color::Palette(i), Some(p)) = (c, self.palette) { c = Color::Rgb(p[usize::from(i)]); } match c { Color::None => {} Color::Palette(i) => write!(f, "\x1b[{prefix};5;{i}m")?, Color::Rgb(v) => write!(f, "\x1b[{prefix};2;{};{};{}m", v.r, v.g, v.b)?, } } Ok(()) } } /// Inline CSS formatter. pub struct HtmlFormatter<'a> { /// Style being formatted. pub style: &'a Style, /// Resolve palette entries to RGB when provided. pub palette: Option<&'a Palette>, } impl fmt::Display for HtmlFormatter<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (prop, mut c) in [ ("color", self.style.fg_color), ("background-color", self.style.bg_color), ("text-decoration-color", self.style.underline_color), ] { if let (Color::Palette(i), Some(p)) = (c, self.palette) { c = Color::Rgb(p[usize::from(i)]); } match c { Color::None => {} Color::Palette(i) => write!(f, "{prop}: var(--vt-palette-{i});")?, Color::Rgb(v) => write!(f, "{prop}: rgb({}, {}, {});", v.r, v.g, v.b)?, } } let flags = self.style.flags; if flags.underline != Underline::None || flags.strikethrough || flags.overline || flags.blink { f.write_str("text-decoration-line:")?; for (b, s) in [ (flags.underline != Underline::None, " underline"), (flags.strikethrough, " line-through"), (flags.overline, " overline"), (flags.blink, " blink"), ] { if b { f.write_str(s)?; } } f.write_str(";")?; } if flags.underline != Underline::None { write!( f, "text-decoration-style: {};", match flags.underline { Underline::None | Underline::Single => "solid", Underline::Double => "double", Underline::Curly => "wavy", Underline::Dotted => "dotted", Underline::Dashed => "dashed", } )?; } for (b, s) in [ (flags.bold, "font-weight: bold;"), (flags.italic, "font-style: italic;"), (flags.faint, "opacity: 0.5;"), (flags.invisible, "visibility: hidden;"), (flags.inverse, "filter: invert(100%);"), ] { if b { f.write_str(s)?; } } Ok(()) } } /// Page-local deduplicated style storage. pub type Set = crate::ref_counted_set::RefCountedSet