Skip to main content

libghostty_vt/
fmt.rs

1//! Format terminal content as plain text, VT sequences, or HTML.
2//!
3//! A formatter captures a reference to a terminal and formatting options.
4//! It can be used repeatedly to produce output that reflects the current
5//! terminal state at the time of each format call.
6use std::{marker::PhantomData, ptr::NonNull};
7
8use crate::{
9    alloc::{Allocator, Bytes, Object},
10    error::{Error, Result, from_result},
11    ffi,
12    selection::Selection,
13    terminal::Terminal,
14};
15
16/// Formatter that formats terminal content.
17#[derive(Debug)]
18pub struct Formatter<'t, 'alloc: 'cb, 'cb: 't> {
19    inner: Object<'alloc, ffi::FormatterImpl>,
20    _terminal: PhantomData<&'t Terminal<'alloc, 'cb>>,
21}
22
23/// Options for [creating a terminal formatter](Formatter::new).
24#[derive(Debug)]
25pub struct FormatterOptions<'t, 's> {
26    inner: ffi::FormatterTerminalOptions,
27    _phan: PhantomData<&'s Selection<'t>>,
28}
29impl<'t, 's> FormatterOptions<'t, 's> {
30    /// Create a new set of options for [creating a terminal formatter](Formatter::new).
31    pub fn new() -> Self {
32        Self {
33            inner: ffi::FormatterTerminalOptions {
34                extra: ffi::FormatterTerminalExtra {
35                    screen: ffi::FormatterScreenExtra {
36                        ..ffi::sized!(ffi::FormatterScreenExtra)
37                    },
38                    ..ffi::sized!(ffi::FormatterTerminalExtra)
39                },
40                ..ffi::sized!(ffi::FormatterTerminalOptions)
41            },
42            _phan: PhantomData,
43        }
44    }
45    /// Specify the output format to emit.
46    pub fn with_format(mut self, value: Format) -> Self {
47        self.inner.emit = value.into();
48        self
49    }
50    /// Specify whether to unwrap soft-wrapped lines.
51    pub fn with_unwrap(mut self, value: bool) -> Self {
52        self.inner.unwrap = value;
53        self
54    }
55    /// Specify whether to trim trailing whitespace on non-blank lines.
56    pub fn with_trim(mut self, value: bool) -> Self {
57        self.inner.trim = value;
58        self
59    }
60    /// Specify the selection to restrict output to a range.
61    ///
62    /// If a selection is not given, the formatter defaults to formatting
63    /// the entire screen.
64    pub fn with_selection(mut self, value: &'s Selection<'t>) -> Self {
65        self.inner.selection = &value.inner;
66        self
67    }
68
69    // --- Extra settings --- //
70
71    /// Specify whether to emit the palette using OSC 4 sequences.
72    pub fn with_palette(mut self, value: bool) -> Self {
73        self.inner.extra.palette = value;
74        self
75    }
76    /// Specify terminal modes that differ from their defaults using CSI h/l.
77    pub fn with_modes(mut self, value: bool) -> Self {
78        self.inner.extra.modes = value;
79        self
80    }
81    /// Specify whether to emit scrolling region state using DECSTBM and DECSLRM sequences.
82    pub fn with_scrolling_region(mut self, value: bool) -> Self {
83        self.inner.extra.scrolling_region = value;
84        self
85    }
86    /// Specify tabstop positions by clearing all tabs and setting each one.
87    pub fn with_tabstops(mut self, value: bool) -> Self {
88        self.inner.extra.tabstops = value;
89        self
90    }
91    /// Specify the present working directory using OSC 7.
92    pub fn with_pwd(mut self, value: bool) -> Self {
93        self.inner.extra.pwd = value;
94        self
95    }
96    /// Specify keyboard modes such as ModifyOtherKeys.
97    pub fn with_keyboard(mut self, value: bool) -> Self {
98        self.inner.extra.keyboard = value;
99        self
100    }
101
102    // --- Screen settings --- //
103
104    /// Specify whether to emit cursor position using CUP (CSI H).
105    pub fn with_cursor(mut self, value: bool) -> Self {
106        self.inner.extra.screen.cursor = value;
107        self
108    }
109    /// Emit current SGR style state based on the cursor's active style_id.
110    pub fn with_style(mut self, value: bool) -> Self {
111        self.inner.extra.screen.style = value;
112        self
113    }
114    /// Emit current hyperlink state using OSC 8 sequences.
115    pub fn with_hyperlink(mut self, value: bool) -> Self {
116        self.inner.extra.screen.hyperlink = value;
117        self
118    }
119    /// Emit character protection mode using DECSCA.
120    pub fn with_protection(mut self, value: bool) -> Self {
121        self.inner.extra.screen.protection = value;
122        self
123    }
124    /// Emit Kitty keyboard protocol state using CSI > u and CSI = sequences.
125    pub fn with_kitty_keyboard(mut self, value: bool) -> Self {
126        self.inner.extra.screen.kitty_keyboard = value;
127        self
128    }
129    /// Emit character set designations and invocations.
130    pub fn with_charsets(mut self, value: bool) -> Self {
131        self.inner.extra.screen.charsets = value;
132        self
133    }
134}
135
136impl<'t, 'alloc: 'cb, 'cb: 't> Formatter<'t, 'alloc, 'cb> {
137    /// Create a formatter for a terminal's active screen.
138    pub fn new(
139        terminal: &'t Terminal<'alloc, 'cb>,
140        opts: FormatterOptions<'t, '_>,
141    ) -> Result<Self> {
142        // SAFETY: A NULL allocator is always valid
143        unsafe { Self::new_inner(std::ptr::null(), terminal, opts) }
144    }
145
146    /// Create a formatter for a terminal's active screen.
147    ///
148    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
149    /// regarding custom memory management and lifetimes.
150    pub fn new_with_alloc<'ctx: 'alloc>(
151        alloc: &'alloc Allocator<'ctx>,
152        terminal: &'t Terminal<'alloc, 'cb>,
153        opts: FormatterOptions,
154    ) -> Result<Self> {
155        // SAFETY: Borrow checking should forbid invalid allocators
156        unsafe { Self::new_inner(alloc.to_raw(), terminal, opts) }
157    }
158
159    unsafe fn new_inner(
160        alloc: *const ffi::Allocator,
161        terminal: &'t Terminal<'alloc, 'cb>,
162        opts: FormatterOptions,
163    ) -> Result<Self> {
164        let mut raw: ffi::Formatter = std::ptr::null_mut();
165
166        let result = unsafe {
167            ffi::ghostty_formatter_terminal_new(
168                alloc,
169                &raw mut raw,
170                terminal.inner.as_raw(),
171                opts.inner,
172            )
173        };
174        from_result(result)?;
175
176        Ok(Self {
177            inner: Object::new(raw)?,
178            _terminal: PhantomData,
179        })
180    }
181
182    /// Run the formatter and return an allocated buffer with the output.
183    ///
184    /// Each call formats the current terminal state. The buffer is allocated
185    /// using the provided allocator (or the default allocator if `None`).
186    pub fn format_alloc<'a, 'ctx: 'a>(
187        &mut self,
188        alloc: Option<&'a Allocator<'ctx>>,
189    ) -> Result<Bytes<'a>> {
190        let alloc = if let Some(alloc) = alloc {
191            alloc.to_raw()
192        } else {
193            std::ptr::null()
194        };
195
196        let mut bytes = std::ptr::null_mut();
197        let mut len = 0usize;
198        let result = unsafe {
199            ffi::ghostty_formatter_format_alloc(
200                self.inner.as_raw(),
201                alloc,
202                std::ptr::from_mut(&mut bytes),
203                std::ptr::from_mut(&mut len),
204            )
205        };
206        from_result(result)?;
207
208        let ptr = NonNull::new(bytes).ok_or(Error::OutOfMemory)?;
209        Ok(unsafe { Bytes::from_raw_parts(ptr, len, alloc) })
210    }
211
212    /// Run the formatter and produce output into the caller-provided buffer.
213    ///
214    /// Each call formats the current terminal state. If the buffer is too small,
215    /// returns `Err(Error::OutOfSpace { required })` where `required` is the
216    /// required size. The caller can then retry with a larger buffer.
217    pub fn format_buf(&mut self, buf: &mut [u8]) -> Result<usize> {
218        let mut len = 0usize;
219        let result = unsafe {
220            ffi::ghostty_formatter_format_buf(
221                self.inner.as_raw(),
222                std::ptr::from_mut(buf).cast(),
223                buf.len(),
224                std::ptr::from_mut(&mut len),
225            )
226        };
227        from_result(result)?;
228        Ok(len)
229    }
230
231    /// Query the required buffer size for the formatted output.
232    ///
233    /// The result can be used to create a sufficiently large buffer
234    /// for [`Formatter::format_buf`].
235    pub fn format_len(&mut self) -> Result<usize> {
236        let mut len = 0usize;
237        let result = unsafe {
238            ffi::ghostty_formatter_format_buf(
239                self.inner.as_raw(),
240                std::ptr::null_mut(),
241                0,
242                std::ptr::from_mut(&mut len),
243            )
244        };
245        // This should always fail with OutOfSpace.
246        match from_result(result) {
247            Err(Error::OutOfSpace { .. }) => Ok(len),
248            Err(e) => Err(e),
249            Ok(()) => Err(Error::InvalidValue),
250        }
251    }
252}
253
254impl Drop for Formatter<'_, '_, '_> {
255    fn drop(&mut self) {
256        unsafe { ffi::ghostty_formatter_free(self.inner.as_raw()) }
257    }
258}
259
260/// Output format.
261#[repr(u32)]
262#[derive(Debug, Clone, Copy, PartialEq, Eq, int_enum::IntEnum)]
263pub enum Format {
264    /// Plain text (no escape sequences).
265    Plain = ffi::FormatterFormat::PLAIN,
266    /// VT sequences preserving colors, styles, URLs, etc.
267    Vt = ffi::FormatterFormat::VT,
268    /// HTML with inline styles.
269    Html = ffi::FormatterFormat::HTML,
270}