//! Cell styles, color resolution, and VT/HTML serialization.
use std::{
fmt,
hash::{Hash, Hasher},
};
use ghostty_vt::{
color::{Palette, Rgb},
sgr::Underline,
};
use crate::page::{Cell, ContentTag};
/// 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 |