Skip to main content

libghostty_vt/
sgr.rs

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