Skip to main content

libghostty_vt/
style.rs

1//! Terminal cell style attributes.
2//!
3//! A style describes the visual attributes of a terminal cell, including
4//! foreground, background, and underline colors, as well as flags for bold,
5//! italic, underline, and other text decorations.
6use std::mem::MaybeUninit;
7
8use crate::{
9    error::{Error, Result},
10    ffi,
11};
12
13/// Style identifier type.
14///
15/// Used to look up the full style from a grid reference.
16/// Obtain this from a cell via [`Cell::style_id`][crate::screen::Cell::style_id].
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct Id(pub(crate) ffi::GhosttyStyleId);
19
20/// Terminal cell style attributes.
21///
22/// A style describes the visual attributes of a terminal cell, including
23/// foreground, background, and underline colors, as well as flags for bold,
24/// italic, underline, and other text decorations.
25#[expect(
26    clippy::struct_excessive_bools,
27    reason = "style attributes should be just a bunch of bools"
28)]
29#[expect(missing_docs, reason = "self-explanatory")]
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Style {
32    pub fg_color: StyleColor,
33    pub bg_color: StyleColor,
34    pub underline_color: StyleColor,
35    pub bold: bool,
36    pub italic: bool,
37    pub faint: bool,
38    pub blink: bool,
39    pub inverse: bool,
40    pub invisible: bool,
41    pub strikethrough: bool,
42    pub overline: bool,
43    pub underline: Underline,
44}
45
46impl Style {
47    /// Check if a style is the default style.
48    ///
49    /// Returns true if all colors are unset and all flags are off.
50    #[must_use]
51    pub fn is_default(self) -> bool {
52        let raw = ffi::GhosttyStyle::from(self);
53        unsafe { ffi::ghostty_style_is_default(&raw const raw) }
54    }
55}
56
57impl Default for Style {
58    fn default() -> Self {
59        let mut style = MaybeUninit::zeroed();
60        unsafe {
61            ffi::ghostty_style_default(style.as_mut_ptr());
62        }
63
64        // SAFETY: We trust the function above to initialize everything correctly
65        Self::try_from(unsafe { style.assume_init() })
66            .expect("ghostty_style_default to init valid Style")
67    }
68}
69
70/// A color used in a style attribute.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum StyleColor {
73    /// Unset.
74    None,
75    /// Palette index.
76    Palette(PaletteIndex),
77    /// Direct RGB value.
78    Rgb(RgbColor),
79}
80
81/// RGB color value.
82#[repr(C)]
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
84pub struct RgbColor {
85    /// Red color component (0-255)
86    pub r: u8,
87    /// Green color component (0-255)
88    pub g: u8,
89    /// Blue color component (0-255)
90    pub b: u8,
91}
92
93/// Palette color index (0-255).
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95pub struct PaletteIndex(pub u8);
96
97impl PaletteIndex {
98    #![expect(clippy::cast_possible_truncation, reason = "bindgen ain't perfect")]
99    #![expect(missing_docs, reason = "self-explanatory")]
100    pub const BLACK: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BLACK as u8);
101    pub const RED: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_RED as u8);
102    pub const GREEN: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_GREEN as u8);
103    pub const YELLOW: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_YELLOW as u8);
104    pub const BLUE: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BLUE as u8);
105    pub const MAGENTA: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_MAGENTA as u8);
106    pub const CYAN: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_CYAN as u8);
107    pub const WHITE: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_WHITE as u8);
108    pub const BRIGHT_BLACK: PaletteIndex =
109        PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_BLACK as u8);
110    pub const BRIGHT_RED: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_RED as u8);
111    pub const BRIGHT_GREEN: PaletteIndex =
112        PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_GREEN as u8);
113    pub const BRIGHT_YELLOW: PaletteIndex =
114        PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW as u8);
115    pub const BRIGHT_BLUE: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_BLUE as u8);
116    pub const BRIGHT_MAGENTA: PaletteIndex =
117        PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA as u8);
118    pub const BRIGHT_CYAN: PaletteIndex = PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_CYAN as u8);
119    pub const BRIGHT_WHITE: PaletteIndex =
120        PaletteIndex(ffi::GHOSTTY_COLOR_NAMED_BRIGHT_WHITE as u8);
121}
122
123/// Underline style types.
124#[repr(u32)]
125#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, int_enum::IntEnum)]
126#[non_exhaustive]
127#[expect(missing_docs, reason = "self-explanatory")]
128pub enum Underline {
129    None = ffi::GhosttySgrUnderline_GHOSTTY_SGR_UNDERLINE_NONE,
130    Single = ffi::GhosttySgrUnderline_GHOSTTY_SGR_UNDERLINE_SINGLE,
131    Double = ffi::GhosttySgrUnderline_GHOSTTY_SGR_UNDERLINE_DOUBLE,
132    Curly = ffi::GhosttySgrUnderline_GHOSTTY_SGR_UNDERLINE_CURLY,
133    Dotted = ffi::GhosttySgrUnderline_GHOSTTY_SGR_UNDERLINE_DOTTED,
134    Dashed = ffi::GhosttySgrUnderline_GHOSTTY_SGR_UNDERLINE_DASHED,
135}
136
137//----------------------------------
138// Conversion to and from FFI types
139//----------------------------------
140
141impl TryFrom<ffi::GhosttyStyle> for Style {
142    type Error = Error;
143    fn try_from(value: ffi::GhosttyStyle) -> Result<Self> {
144        Ok(Self {
145            fg_color: StyleColor::try_from(value.fg_color)?,
146            bg_color: StyleColor::try_from(value.bg_color)?,
147            underline_color: StyleColor::try_from(value.underline_color)?,
148            bold: value.bold,
149            italic: value.italic,
150            faint: value.faint,
151            blink: value.blink,
152            inverse: value.inverse,
153            invisible: value.invisible,
154            strikethrough: value.strikethrough,
155            overline: value.overline,
156            #[expect(clippy::cast_sign_loss, reason = "bindgen ain't perfect")]
157            underline: Underline::try_from(value.underline as u32)
158                .map_err(|_| Error::InvalidValue)?,
159        })
160    }
161}
162
163impl From<Style> for ffi::GhosttyStyle {
164    fn from(value: Style) -> Self {
165        Self {
166            size: std::mem::size_of::<Self>(),
167            fg_color: value.fg_color.into(),
168            bg_color: value.bg_color.into(),
169            underline_color: value.underline_color.into(),
170            bold: value.bold,
171            italic: value.italic,
172            faint: value.faint,
173            blink: value.blink,
174            inverse: value.inverse,
175            invisible: value.invisible,
176            strikethrough: value.strikethrough,
177            overline: value.overline,
178            #[expect(clippy::cast_possible_wrap, reason = "bindgen ain't perfect")]
179            underline: u32::from(value.underline) as i32,
180        }
181    }
182}
183
184impl TryFrom<ffi::GhosttyStyleColor> for StyleColor {
185    type Error = Error;
186    fn try_from(value: ffi::GhosttyStyleColor) -> Result<Self> {
187        Ok(match value.tag {
188            ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_NONE => Self::None,
189            ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_PALETTE => {
190                Self::Palette(PaletteIndex(unsafe { value.value.palette }))
191            }
192            ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_RGB => {
193                Self::Rgb(unsafe { value.value.rgb }.into())
194            }
195            _ => return Err(Error::InvalidValue),
196        })
197    }
198}
199
200impl From<StyleColor> for ffi::GhosttyStyleColor {
201    fn from(value: StyleColor) -> Self {
202        match value {
203            StyleColor::None => Self {
204                tag: ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_NONE,
205                value: ffi::GhosttyStyleColorValue::default(),
206            },
207            StyleColor::Palette(PaletteIndex(palette)) => Self {
208                tag: ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_PALETTE,
209                value: ffi::GhosttyStyleColorValue { palette },
210            },
211            StyleColor::Rgb(rgb) => Self {
212                tag: ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_NONE,
213                value: ffi::GhosttyStyleColorValue { rgb: rgb.into() },
214            },
215        }
216    }
217}
218
219impl From<ffi::GhosttyColorRgb> for RgbColor {
220    fn from(value: ffi::GhosttyColorRgb) -> Self {
221        let ffi::GhosttyColorRgb { r, g, b } = value;
222        Self { r, g, b }
223    }
224}
225
226impl From<RgbColor> for ffi::GhosttyColorRgb {
227    fn from(value: RgbColor) -> Self {
228        let RgbColor { r, g, b } = value;
229        Self { r, g, b }
230    }
231}