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}