Skip to main content

libghostty_vt/
sgr.rs

1//! Handling SGR (Select Graphic Rendition) escape sequences.
2
3use crate::{
4    alloc::{Allocator, Object},
5    error::{Error, Result, from_result},
6    ffi,
7    style::{PaletteIndex, RgbColor, Underline},
8};
9
10/// SGR (Select Graphic Rendition) attribute parser.
11///
12/// SGR sequences are the syntax used to set styling attributes such as bold,
13/// italic, underline, and colors for text in terminal emulators. For example,
14/// you may be familiar with sequences like `ESC[1;31m`. The 1;31 is the SGR
15/// attribute list.
16///
17/// The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`)
18/// and returns individual text attributes like bold, italic, colors, etc. It
19/// supports both semicolon (`;`) and colon (`:`) separators, possibly mixed,
20/// and handles various color formats including 8-color, 16-color, 256-color,
21/// X11 named colors, and RGB in multiple formats.
22///
23/// # Example
24/// ```rust
25/// use libghostty_vt::sgr::{Parser, Attribute};
26///
27/// let mut parser = Parser::new().unwrap();
28/// parser.set_params(&[1, 31], None).unwrap();
29///
30/// while let Some(attr) = parser.next().unwrap() {
31///     match attr {
32///         Attribute::Bold => println!("Bold enabled"),
33///         Attribute::Fg8(color) => println!("Foreground color: {color:?}"),
34///         _ => {},
35///     }
36/// }
37/// ```
38#[derive(Debug)]
39pub struct Parser<'alloc>(Object<'alloc, ffi::SgrParserImpl>);
40
41impl<'alloc> Parser<'alloc> {
42    /// Create a new SGR parser.
43    pub fn new() -> Result<Self> {
44        // SAFETY: A NULL allocator is always valid
45        unsafe { Self::new_inner(std::ptr::null()) }
46    }
47
48    /// Create a new SGR parser with a custom allocator.
49    ///
50    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
51    /// regarding custom memory management and lifetimes.
52    pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
53        // SAFETY: Borrow checking should forbid invalid allocators
54        unsafe { Self::new_inner(alloc.to_raw()) }
55    }
56
57    unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
58        let mut raw: ffi::SgrParser = std::ptr::null_mut();
59        let result = unsafe { ffi::ghostty_sgr_new(alloc, &raw mut raw) };
60        from_result(result)?;
61        Ok(Self(Object::new(raw)?))
62    }
63
64    /// Set SGR parameters for parsing.
65    ///
66    /// Parameters are the numeric values from a CSI SGR sequence (e.g., for `ESC[1;31m`, params
67    /// would be `[1, 31]`).
68    ///
69    /// The `separators` slice optionally specifies the separator type for each parameter position.
70    /// Each byte should be either `b';'` for semicolon or `b':'` for colon.
71    /// This is needed for certain color formats that use colon separators (e.g., `ESC[4:3m`
72    /// for curly underline). Any invalid separator values are treated as semicolons.
73    ///
74    /// If `separators` is `None`, all parameters are assumed to be semicolon-separated.
75    ///
76    /// After calling this function, the parser is automatically reset and ready to iterate from
77    /// the beginning.
78    ///
79    /// # Panics
80    ///
81    /// **Panics** if `separators` is not `None` and is not the same length as `params`.
82    pub fn set_params(&mut self, params: &[u16], separators: Option<&[u8]>) -> Result<()> {
83        let sep = match separators {
84            Some(seps) => {
85                assert!(
86                    seps.len() == params.len(),
87                    "separators length must equal params length"
88                );
89                seps.as_ptr().cast()
90            }
91            None => std::ptr::null(),
92        };
93        let result = unsafe {
94            ffi::ghostty_sgr_set_params(self.0.as_raw(), params.as_ptr(), sep, params.len())
95        };
96        from_result(result)
97    }
98
99    /// Get the next SGR attribute.
100    ///
101    /// Parses and returns the next attribute from the parameter list.
102    /// Call this function repeatedly until it returns `None` to process all
103    /// attributes in the sequence.
104    ///
105    /// This cannot be expressed as a regular iterator since the returned
106    /// attribute borrows memory from the parser directly.
107    #[expect(
108        clippy::should_implement_trait,
109        reason = "lending `next` cannot implement trait"
110    )]
111    pub fn next(&mut self) -> Result<Option<Attribute<'_>>> {
112        let mut raw_attr = ffi::SgrAttribute::default();
113        let has_next = unsafe { ffi::ghostty_sgr_next(self.0.as_raw(), &raw mut raw_attr) };
114        if has_next {
115            // This shouldn't really *ever* fail, so the fact it failed
116            // suggests we should stop anyways.
117            Ok(Some(Attribute::from_raw(raw_attr)?))
118        } else {
119            Ok(None)
120        }
121    }
122
123    /// Reset an SGR parser instance to the beginning of the parameter list.
124    ///
125    /// Resets the parser's iteration state without clearing the parameters.
126    /// After calling this, [`Parser::next`] will start from the beginning of the
127    /// parameter list again.
128    pub fn reset(&mut self) {
129        unsafe { ffi::ghostty_sgr_reset(self.0.as_raw()) }
130    }
131}
132
133impl Drop for Parser<'_> {
134    fn drop(&mut self) {
135        unsafe { ffi::ghostty_sgr_free(self.0.as_raw()) }
136    }
137}
138
139/// An SGR attribute.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141#[non_exhaustive]
142#[expect(missing_docs, reason = "missing upstream docs")]
143pub enum Attribute<'p> {
144    Unset,
145    Unknown(Unknown<'p>),
146    Bold,
147    ResetBold,
148    Italic,
149    ResetItalic,
150    Faint,
151    Underline(Underline),
152    UnderlineColor(RgbColor),
153    UnderlineColor256(PaletteIndex),
154    ResetUnderlineColor,
155    Overline,
156    ResetOverline,
157    Blink,
158    ResetBlink,
159    Inverse,
160    ResetInverse,
161    Invisible,
162    ResetInvisible,
163    Strikethrough,
164    ResetStrikethrough,
165    DirectColorFg(RgbColor),
166    DirectColorBg(RgbColor),
167    Bg8(PaletteIndex),
168    Fg8(PaletteIndex),
169    ResetFg,
170    ResetBg,
171    BrightBg8(PaletteIndex),
172    BrightFg8(PaletteIndex),
173    Bg256(PaletteIndex),
174    Fg256(PaletteIndex),
175}
176
177impl Attribute<'_> {
178    /// This should never return None, but just to be safe.
179    fn from_raw(value: ffi::SgrAttribute) -> Result<Self> {
180        Ok(match value.tag {
181            0 => Self::Unset,
182            1 => Self::Unknown(unsafe { value.value.unknown }.into()),
183            2 => Self::Bold,
184            3 => Self::ResetBold,
185            4 => Self::Italic,
186            5 => Self::ResetItalic,
187            6 => Self::Faint,
188            7 => Self::Underline(
189                Underline::try_from(unsafe { value.value.underline })
190                    .map_err(|_| Error::InvalidValue)?,
191            ),
192            8 => Self::UnderlineColor(unsafe { value.value.underline_color }.into()),
193            9 => Self::UnderlineColor256(PaletteIndex(unsafe { value.value.underline_color_256 })),
194            10 => Self::ResetUnderlineColor,
195            11 => Self::Overline,
196            12 => Self::ResetOverline,
197            13 => Self::Blink,
198            14 => Self::ResetBlink,
199            15 => Self::Inverse,
200            16 => Self::ResetInverse,
201            17 => Self::Invisible,
202            18 => Self::ResetInvisible,
203            19 => Self::Strikethrough,
204            20 => Self::ResetStrikethrough,
205            21 => Self::DirectColorFg(unsafe { value.value.direct_color_fg }.into()),
206            22 => Self::DirectColorBg(unsafe { value.value.direct_color_bg }.into()),
207            23 => Self::Bg8(PaletteIndex(unsafe { value.value.bg_8 })),
208            24 => Self::Fg8(PaletteIndex(unsafe { value.value.fg_8 })),
209            25 => Self::ResetFg,
210            26 => Self::ResetBg,
211            27 => Self::BrightBg8(PaletteIndex(unsafe { value.value.bright_bg_8 })),
212            28 => Self::BrightFg8(PaletteIndex(unsafe { value.value.bright_fg_8 })),
213            29 => Self::Bg256(PaletteIndex(unsafe { value.value.bg_256 })),
214            30 => Self::Fg256(PaletteIndex(unsafe { value.value.fg_256 })),
215            _ => return Err(Error::InvalidValue),
216        })
217    }
218}
219
220/// Unknown SGR attribute data.
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub struct Unknown<'p> {
223    /// Full parameter list.
224    pub full: &'p [u16],
225    /// Partial list where parsing encountered an unknown or invalid sequence.
226    pub partial: &'p [u16],
227}
228
229impl From<ffi::SgrUnknown> for Unknown<'_> {
230    fn from(value: ffi::SgrUnknown) -> Self {
231        // SAFETY: We trust libghostty to give us two valid slices
232        // of u16s that last at least as long as the current iteration,
233        // which is guaranteed by Rust's mutation XOR sharability property
234        // (e.g. one cannot reset the parser when this object still
235        // borrows the parser mutably).
236        let full = unsafe { std::slice::from_raw_parts(value.full_ptr, value.full_len) };
237        let partial = unsafe { std::slice::from_raw_parts(value.partial_ptr, value.partial_len) };
238        Self { full, partial }
239    }
240}