Skip to main content

libghostty_vt/
terminal.rs

1//! Types and functions around terminal state management.
2
3use std::{marker::PhantomData, mem::MaybeUninit, ptr::NonNull};
4
5use crate::{
6    alloc::{Allocator, Object},
7    error::{Error, Result, from_optional_result_uninit, from_result, from_result_with_len},
8    ffi::{self, TerminalData as Data, TerminalOption as Opt},
9    key,
10    screen::{GridRef, Screen, TrackedGridRef},
11    style::{self, Palette, RawPalette, RgbColor},
12};
13
14#[doc(inline)]
15pub use ffi::{SizeReportSize, TerminalScrollbar as Scrollbar};
16
17/// Complete terminal emulator state and rendering.
18///
19/// A terminal instance manages the full emulator state including the screen,
20/// scrollback, cursor, styles, modes, and VT stream processing.
21///
22/// Once a terminal session is up and running, you can configure a key encoder
23/// to write keyboard input via [`key::Encoder::set_options_from_terminal`].
24///
25/// ## Example: VT stream processing
26///
27/// ```
28/// use libghostty_vt::{Terminal, TerminalOptions};
29///
30/// // Create a terminal
31/// let mut terminal = Terminal::new(TerminalOptions {
32///     cols: 80,
33///     rows: 24,
34///     max_scrollback: 0,
35/// }).unwrap();
36///
37/// // Feed VT data into the terminal
38/// terminal.vt_write(b"Hello, World!\r\n");
39///
40/// // ANSI color codes: ESC[1;32m = bold green, ESC[0m = reset
41/// terminal.vt_write(b"\x1b[1;32mGreen Text\x1b[0m\r\n");
42///
43/// // Cursor positioning: ESC[1;1H = move to row 1, column 1
44/// terminal.vt_write(b"\x1b[1;1HTop-left corner\r\n");
45///
46/// // Cursor movement: ESC[5B = move down 5 lines
47/// terminal.vt_write(b"\x1b[5B");
48/// terminal.vt_write(b"Moved down!\r\n");
49///
50/// // Erase line: ESC[2K = clear entire line
51/// terminal.vt_write(b"\x1b[2K");
52/// terminal.vt_write(b"New content\r\n");
53///
54/// // Multiple lines
55/// terminal.vt_write(b"Line A\r\nLine B\r\nLine C\r\n");
56/// ```
57///
58/// # Effects
59///
60/// By default, the terminal sequence processing with [`Terminal::vt_write`]
61/// only process sequences that directly affect terminal state and ignores
62/// sequences that have side effect behavior or require responses. These
63/// sequences include things like bell characters, title changes, device
64/// attributes queries, and more. To handle these sequences, the user
65/// must configure "effects."
66///
67/// Effects are callbacks that the terminal invokes in response to VT sequences
68/// processed during [`Terminal::vt_write`]. They let the embedding application
69/// react to terminal-initiated events such as bell characters, title changes,
70/// device status report responses, and more.
71///
72/// Each effect is registered with its corresponding `Terminal::on_<effect>`
73/// function, which accepts a closure with access to the terminal state and
74/// possibly other parameters. Some examples include [`Terminal::on_bell`]
75/// and [`Terminal::on_pty_write`].
76///
77/// All callbacks are invoked synchronously during [`Terminal::vt_write`].
78/// Callbacks must be very careful to not block for too long or perform
79/// expensive operations, since they are blocking further IO processing.
80///
81/// ## Shared state
82///
83/// **Unlike the C API**, you *cannot* specify arbitrary user data that's
84/// shared between all callbacks, mainly because a safe, idiomatic Rust
85/// equivalent of this pattern is very difficult to implement and use
86/// due to Rust's much stricter safety guarantees. In turn, we use the
87/// user data internally for callback dispatch purposes.
88///
89/// You should instead use types that allow safe *interior mutability*
90/// (e.g. [`Cell`](std::cell::Cell) or [`RefCell`](std::cell::RefCell))
91/// and pass a shared reference into each effect handler that needs to mutate
92/// the shared state. Note that reference counting mechanisms like
93/// [`Rc`](std::rc::Rc) and [`Arc`](std::sync::Arc) are optional.
94///
95/// ## Example: Registering effects and processing VT data
96///
97/// ```rust
98/// use std::cell::Cell;
99/// use libghostty_vt::{Terminal, TerminalOptions};
100///
101/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
102/// // Set up a simple bell counter.
103/// //
104/// // `usize` is a simple, `Copy`able type, which means `Cell`s are
105/// // perfectly suitable here. More complex, non-`Copy` types should
106/// // use `RefCell`s instead.
107/// //
108/// // This has to be done before the terminal is created, since
109/// // its effect handlers will continue to refer to the bell counter
110/// // during the lifetime of the terminal.
111/// let bell_count = Cell::new(0usize);
112///
113/// let mut terminal = Terminal::new(TerminalOptions {
114///     cols: 80,
115///     rows: 24,
116///     max_scrollback: 0,
117/// })?;
118///
119/// terminal
120///     .on_pty_write(|_term, data| {
121///         println!("Replying {} bytes to the PTY", data.len());
122///     })?
123///    .on_bell({
124///        // Explicitly borrow the bell count, or otherwise `move`
125///        // will attempt to capture the entire `Cell` and cause a
126///        // compiler error
127///        let bell_count = &bell_count;
128///        move |_term| {
129///            bell_count.update(|v| v + 1);
130///            println!("Bell! (count = {})", bell_count.get())
131///        }
132///     })?
133///    .on_title_changed(|term| {
134///        // Query the cursor position to confirm the terminal processed the
135///        // title change (the title itself is tracked by the embedder via the
136///        // OSC parser or its own state).
137///        let col = term.cursor_x().unwrap();
138///        println!("Title changed! (cursor at col {col})");
139///    })?;
140///
141/// // Feed VT data that triggers effects:
142/// // 1. Bell (BEL = 0x07)
143/// terminal.vt_write(b"\x07");
144/// // 2. Title change (OSC 2 ; <title> ST)
145/// terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
146/// // 3. Device status report (DECRQM for wraparound mode ?7)
147/// //    triggers write_pty with the response
148/// terminal.vt_write(b"\x1B[?7$p");
149/// // 4. Another bell to show the counter increments
150/// terminal.vt_write(b"\x07");
151///
152/// assert_eq!(bell_count.get(), 2);
153/// # Ok(())}
154/// ```
155///
156/// # Color theme
157///
158/// The terminal maintains a set of colors used for rendering: a foreground
159/// color, a background color, a cursor color, and a 256-color palette. Each
160/// of these has two layers: a **default** value set by the embedder, and an
161/// **override** value that programs running in the terminal can set via OSC
162/// escape sequences (e.g. OSC 10/11/12 for foreground/background/cursor,
163/// OSC 4 for individual palette entries).
164///
165/// ## Default colors
166///
167/// Use [`Terminal::set_default_fg_color`], [`Terminal::set_default_bg_color`],
168/// [`Terminal::set_default_cursor_color`] and [`Terminal::set_default_color_palette`]
169/// to configure the default colors. These represent the theme or configuration
170/// chosen by the embedder. Passing `None` clears the default, leaving the color
171/// unset.
172///
173/// For the palette, passing `None` resets to the built-in default palette.
174/// The palette set operation preserves any per-index OSC overrides that programs
175/// have applied; only unmodified indices are updated.
176///
177/// ## Reading colors
178///
179/// Use functions like [`Terminal::default_cursor_color`],
180/// [`Terminal::bg_color`], [`Terminal::default_color_palette`], etc. to read
181/// colors. There are two variants for each color: the **effective** value
182/// (which returns the OSC override if one is active, otherwise the default)
183/// and the **default** value (which ignores any OSC overrides).
184///
185/// For foreground, background, and cursor colors, the getters return `Ok(None)`
186/// if no color is configured (neither a default nor an OSC override).
187/// The palette getters always succeed since the palette always has a value
188/// (the built-in default if nothing else is set).
189///
190/// ## Setting a color theme
191///
192/// ```
193/// use libghostty_vt::{
194///     style::{RgbColor, PaletteIndex},
195///     Error,
196///     Terminal,
197/// };
198///
199/// fn set_color_theme(terminal: &mut Terminal<'_, '_>) -> Result<(), Error> {
200///     // Set default foreground (light gray) and background (dark)
201///     terminal
202///         .set_default_fg_color(Some(
203///             RgbColor { r: 0xDD, g: 0xDD, b: 0xDD }
204///         ))?
205///         .set_default_bg_color(Some(
206///             RgbColor { r: 0x1E, g: 0x1E, b: 0x2E }
207///         ))?
208///         .set_default_cursor_color(Some(
209///             RgbColor { r: 0xF5, g: 0xE0, b: 0xDC }
210///         ))?;
211///     
212///     // Set a custom palette — start from the built-in default and override
213///     // the first 8 entries with a custom dark theme.
214///     let mut palette = terminal.default_color_palette()?;
215///     palette.set(PaletteIndex::BLACK, RgbColor { r: 0x45, g: 0x47, b: 0x5A });
216///     palette.set(PaletteIndex::RED, RgbColor { r: 0xF3, g: 0x8B, b: 0xA8 });
217///     palette.set(PaletteIndex::GREEN, RgbColor { r: 0xA6, g: 0xE3, b: 0xA1 });
218///     palette.set(PaletteIndex::YELLOW, RgbColor { r: 0xF9, g: 0xE2, b: 0xAF });
219///     palette.set(PaletteIndex::BLUE, RgbColor { r: 0x89, g: 0xB4, b: 0xFA });
220///     palette.set(PaletteIndex::MAGENTA, RgbColor { r: 0xF5, g: 0xC2, b: 0xE7 });
221///     palette.set(PaletteIndex::CYAN, RgbColor { r: 0x94, g: 0xE2, b: 0xD5 });
222///     palette.set(PaletteIndex::WHITE, RgbColor { r: 0xBA, g: 0xC2, b: 0xDE });
223///     
224///     terminal.set_default_color_palette(Some(palette))?;
225///     Ok(())
226/// }
227/// ```
228///
229#[derive(Debug)]
230pub struct Terminal<'alloc: 'cb, 'cb> {
231    pub(crate) inner: Object<'alloc, ffi::TerminalImpl>,
232    // Keep callbacks in a heap allocation so C can store a userdata pointer
233    // to the VTable itself. That pointer remains stable even if Terminal moves.
234    vtable: Box<VTable<'alloc, 'cb>>,
235}
236
237/// Terminal initialization options.
238#[derive(Clone, Copy, Debug)]
239pub struct Options {
240    /// Terminal width in cells. Must be greater than zero.
241    pub cols: u16,
242    /// Terminal height in cells. Must be greater than zero.
243    pub rows: u16,
244    /// Maximum number of lines to keep in scrollback history.
245    pub max_scrollback: usize,
246}
247
248impl From<Options> for ffi::TerminalOptions {
249    fn from(value: Options) -> Self {
250        Self {
251            cols: value.cols,
252            rows: value.rows,
253            max_scrollback: value.max_scrollback,
254        }
255    }
256}
257
258/// Default visual style used when the cursor style is reset.
259#[repr(u32)]
260#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
261#[non_exhaustive]
262pub enum CursorStyle {
263    /// Bar cursor (DECSCUSR 5, 6).
264    Bar = ffi::TerminalCursorStyle::BAR,
265    /// Block cursor (DECSCUSR 1, 2).
266    Block = ffi::TerminalCursorStyle::BLOCK,
267    /// Underline cursor (DECSCUSR 3, 4).
268    Underline = ffi::TerminalCursorStyle::UNDERLINE,
269    /// Hollow block cursor.
270    BlockHollow = ffi::TerminalCursorStyle::BLOCK_HOLLOW,
271}
272
273impl<'alloc: 'cb, 'cb> Terminal<'alloc, 'cb> {
274    /// Create a new terminal instance.
275    pub fn new(opts: Options) -> Result<Self> {
276        // SAFETY: A NULL allocator is always valid
277        unsafe { Self::new_inner(std::ptr::null(), opts) }
278    }
279
280    /// Create a new terminal instance with a custom allocator.
281    ///
282    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
283    /// regarding custom memory management and lifetimes.
284    pub fn new_with_alloc<'ctx: 'alloc>(
285        alloc: &'alloc Allocator<'ctx>,
286        opts: Options,
287    ) -> Result<Self> {
288        // SAFETY: Borrow checking should forbid invalid allocators
289        unsafe { Self::new_inner(alloc.to_raw(), opts) }
290    }
291
292    unsafe fn new_inner(alloc: *const ffi::Allocator, opts: Options) -> Result<Self> {
293        let mut raw: ffi::Terminal = std::ptr::null_mut();
294        let result = unsafe { ffi::ghostty_terminal_new(alloc, &raw mut raw, opts.into()) };
295        from_result(result)?;
296        Ok(Self {
297            inner: Object::new(raw)?,
298            vtable: Box::new(VTable::default()),
299        })
300    }
301
302    /// Write VT-encoded data to the terminal for processing.
303    ///
304    /// Feeds raw bytes through the terminal's VT stream parser, updating
305    /// terminal state accordingly. By default, sequences that require output
306    /// (queries, device status reports) are silently ignored.
307    /// Use [`Terminal::on_pty_write`] to install a callback that receives
308    /// response data.
309    ///
310    /// This never fails. Any erroneous input or errors in processing the input
311    /// are logged internally but do not cause this function to fail because
312    /// this input is assumed to be untrusted and from an external source; so
313    /// the primary goal is to keep the terminal state consistent and not allow
314    /// malformed input to corrupt or crash.    
315    pub fn vt_write(&mut self, data: &[u8]) {
316        unsafe { ffi::ghostty_terminal_vt_write(self.inner.as_raw(), data.as_ptr(), data.len()) }
317    }
318
319    /// Resize the terminal to the given dimensions.
320    ///
321    /// Changes the number of columns and rows in the terminal. The primary
322    /// screen will reflow content if wraparound mode is enabled; the alternate
323    /// screen does not reflow. If the dimensions are unchanged, this is a no-op.
324    ///
325    /// This also updates the terminal's pixel dimensions (used for image
326    /// protocols and size reports), disables synchronized output mode (allowed
327    /// by the spec so that resize results are shown immediately), and sends an
328    /// in-band size report if mode 2048 is enabled.
329    pub fn resize(
330        &mut self,
331        cols: u16,
332        rows: u16,
333        cell_width_px: u32,
334        cell_height_px: u32,
335    ) -> Result<()> {
336        let result = unsafe {
337            ffi::ghostty_terminal_resize(
338                self.inner.as_raw(),
339                cols,
340                rows,
341                cell_width_px,
342                cell_height_px,
343            )
344        };
345        from_result(result)
346    }
347
348    /// Perform a full reset of the terminal (RIS).
349    ///
350    /// Resets all terminal state back to its initial configuration,
351    /// including modes, scrollback, scrolling region, and screen contents.
352    /// The terminal dimensions are preserved.
353    pub fn reset(&mut self) {
354        unsafe { ffi::ghostty_terminal_reset(self.inner.as_raw()) }
355    }
356
357    /// Scroll the terminal viewport.
358    pub fn scroll_viewport(&mut self, scroll: ScrollViewport) {
359        unsafe { ffi::ghostty_terminal_scroll_viewport(self.inner.as_raw(), scroll.into()) }
360    }
361
362    /// Resolve a point in the terminal grid to a grid reference.
363    ///
364    /// Resolves the given point (which can be in active, viewport, screen,
365    /// or history coordinates) to a grid reference for that location. Use
366    /// [`GridRef::cell`] and [`GridRef::row`] to extract the cell and row.
367    ///
368    /// Lookups in the active region and viewport are fast. Lookups in the
369    /// screen and history may require traversing the full scrollback page
370    /// list to resolve the y coordinate, so they can be expensive for large
371    /// scrollback buffers.
372    ///
373    /// This function isn't meant to be used as the core of render loop. It
374    /// isn't built to sustain the framerates needed for rendering large
375    /// screens. Use the [render state API](crate::render::RenderState) for
376    /// that. This API is instead meant for less strictly performance-sensitive
377    /// use cases.
378    pub fn grid_ref(&self, point: Point) -> Result<GridRef<'_>> {
379        let mut grid_ref = ffi::sized!(ffi::GridRef);
380        let result = unsafe {
381            ffi::ghostty_terminal_grid_ref(self.inner.as_raw(), point.into(), &raw mut grid_ref)
382        };
383        from_result(result)?;
384        Ok(unsafe { GridRef::from_raw(grid_ref) })
385    }
386
387    /// Create an owned tracked grid reference for a terminal point.
388    ///
389    /// This is the tracked variant of [`Terminal::grid_ref`]. The returned handle
390    /// follows the referenced cell as the terminal's page list is modified:
391    /// scrolling, pruning, resize/reflow, and other page-list operations update
392    /// the tracked reference automatically.
393    ///
394    /// The reference is attached to the terminal screen/page-list that is
395    /// active at creation time.
396    ///
397    /// If the point is outside the requested coordinate space, this returns
398    /// `Err(Error::InvalidValue)`.
399    ///
400    /// If the tracked grid reference outlives this terminal, the handle remains
401    /// valid, but it will always return `false` or `Ok(None)`.
402    pub fn track_grid_ref(&self, point: Point) -> Result<TrackedGridRef> {
403        let mut raw: ffi::TrackedGridRef = std::ptr::null_mut();
404        let result = unsafe {
405            ffi::ghostty_terminal_grid_ref_track(self.inner.as_raw(), point.into(), &raw mut raw)
406        };
407        from_result(result)?;
408
409        let inner = NonNull::new(raw).ok_or(Error::InvalidValue)?;
410        Ok(TrackedGridRef::new(inner, self.inner.ptr))
411    }
412
413    /// Convert a grid reference back to a point in the given coordinate system.
414    ///
415    /// This is the inverse of [`Terminal::grid_ref`]: given a grid reference, it
416    /// returns the x/y coordinates in the requested coordinate system (active,
417    /// viewport, screen, or history).
418    ///
419    /// The grid reference must have been obtained from the same terminal instance.
420    /// Like all grid references, it is only valid until the next mutating
421    /// terminal call.
422    ///
423    /// Not every grid reference is representable in every coordinate system.
424    /// For example, a cell in scrollback history cannot be expressed in active
425    /// coordinates, and a cell that has scrolled off the visible area cannot
426    /// be expressed in viewport coordinates. In these cases, the function
427    /// returns `Ok(None)`.
428    pub fn point_from_grid_ref(
429        &self,
430        grid_ref: &GridRef<'_>,
431        space: PointSpace,
432    ) -> Result<Option<PointCoordinate>> {
433        let mut point = MaybeUninit::<ffi::PointCoordinate>::zeroed();
434        let result = unsafe {
435            ffi::ghostty_terminal_point_from_grid_ref(
436                self.inner.as_raw(),
437                std::ptr::from_ref(&grid_ref.inner),
438                space.into_raw(),
439                point.as_mut_ptr(),
440            )
441        };
442
443        from_optional_result_uninit(result, point).map(|value| value.map(Into::into))
444    }
445
446    /// Get the current value of a terminal mode.
447    pub fn mode(&self, mode: Mode) -> Result<bool> {
448        let mut value = false;
449        let result = unsafe {
450            ffi::ghostty_terminal_mode_get(self.inner.as_raw(), mode.into(), &raw mut value)
451        };
452        from_result(result)?;
453        Ok(value)
454    }
455
456    /// Set the value of a terminal mode.
457    pub fn set_mode(&mut self, mode: Mode, value: bool) -> Result<&mut Self> {
458        let result =
459            unsafe { ffi::ghostty_terminal_mode_set(self.inner.as_raw(), mode.into(), value) };
460        from_result(result)?;
461        Ok(self)
462    }
463
464    /// Compress eligible terminal scrollback.
465    ///
466    /// Incremental mode performs bounded work suitable for an idle callback.
467    /// A pending result means the application should invoke another step while
468    /// the terminal remains idle. A complete result means no continuation is
469    /// needed until `Terminal::compression_activity` changes. Full mode
470    /// performs one synchronous scan and can stall on large scrollback buffers.
471    ///
472    /// Compression is opportunistic. Complete means the pass has finished,
473    /// not that every page was compressed: pages may be unprofitable or
474    /// encounter an allocation or reclamation failure. Compression changes
475    /// only the terminal's storage representation and never its logical
476    /// contents or scrollback limit. Accessing compressed history restores
477    /// it transparently.
478    ///
479    /// This function is not thread-safe with other operations on the same
480    /// terminal. The caller must serialize it with writes, rendering, searches,
481    /// and other terminal access.
482    pub fn compress(&mut self, mode: CompressionMode) -> Result<CompressionResult> {
483        let mut value = ffi::TerminalCompressionResult::UNSUPPORTED;
484        let result = unsafe {
485            ffi::ghostty_terminal_compress(self.inner.as_raw(), mode.into(), &raw mut value)
486        };
487        from_result(result)?;
488        value.try_into().map_err(|_| Error::InvalidValue)
489    }
490
491    /// Return the current compression activity token.
492    ///
493    /// The token is opaque and only equality comparisons are meaningful.
494    /// An embedding application should cache it and restart its compression
495    /// idle delay whenever the value changes. The value may wrap and changes
496    /// in either direction have the same meaning.
497    ///
498    /// This function only observes terminal state.
499    /// It does not perform or schedule compression.
500    pub fn compression_activity(&self) -> Result<CompressionActivity> {
501        let mut value = 0;
502        let result = unsafe {
503            ffi::ghostty_terminal_compression_activity(self.inner.as_raw(), &raw mut value)
504        };
505        from_result(result)?;
506        Ok(CompressionActivity(value))
507    }
508
509    pub(crate) fn get<T>(&self, tag: ffi::TerminalData::Type) -> Result<T> {
510        let mut value = MaybeUninit::<T>::zeroed();
511        let result = unsafe {
512            ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
513        };
514        from_result(result)?;
515        // SAFETY: Value should be initialized after successful call.
516        Ok(unsafe { value.assume_init() })
517    }
518    pub(crate) fn get_optional<T>(&self, tag: ffi::TerminalData::Type) -> Result<Option<T>> {
519        let mut value = MaybeUninit::<T>::zeroed();
520        let result = unsafe {
521            ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
522        };
523        from_optional_result_uninit(result, value)
524    }
525    pub(crate) fn set<T>(&self, tag: ffi::TerminalOption::Type, v: &T) -> Result<()> {
526        let result = unsafe {
527            ffi::ghostty_terminal_set(self.inner.as_raw(), tag, std::ptr::from_ref(v).cast())
528        };
529        from_result(result)
530    }
531    /// Set an option whose ABI expects the pointer value itself, not a pointer
532    /// to Rust storage containing that value.
533    pub(crate) fn set_ptr(
534        &self,
535        tag: ffi::TerminalOption::Type,
536        ptr: *const std::ffi::c_void,
537    ) -> Result<()> {
538        let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr) };
539        from_result(result)
540    }
541    pub(crate) fn set_optional<T>(
542        &self,
543        tag: ffi::TerminalOption::Type,
544        v: Option<&T>,
545    ) -> Result<()> {
546        let ptr = if let Some(v) = v {
547            std::ptr::from_ref(v)
548        } else {
549            std::ptr::null()
550        };
551
552        let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr.cast()) };
553        from_result(result)
554    }
555
556    /// Get the terminal width in cells.
557    pub fn cols(&self) -> Result<u16> {
558        self.get(Data::COLS)
559    }
560    /// Get the terminal height in cells.
561    pub fn rows(&self) -> Result<u16> {
562        self.get(Data::ROWS)
563    }
564    /// Get the cursor column position (inner-indexed).
565    pub fn cursor_x(&self) -> Result<u16> {
566        self.get(Data::CURSOR_X)
567    }
568    /// Get the cursor row position within the active area (inner-indexed).
569    pub fn cursor_y(&self) -> Result<u16> {
570        self.get(Data::CURSOR_Y)
571    }
572    /// Get whether the cursor has a pending wrap (next print will soft-wrap).
573    pub fn is_cursor_pending_wrap(&self) -> Result<bool> {
574        self.get(Data::CURSOR_PENDING_WRAP)
575    }
576    /// Get whether the cursor is visible (DEC mode 25).
577    pub fn is_cursor_visible(&self) -> Result<bool> {
578        self.get(Data::CURSOR_VISIBLE)
579    }
580    /// Get the current SGR style of the cursor.
581    ///
582    /// This is the style that will be applied to newly printed characters.
583    pub fn cursor_style(&self) -> Result<style::Style> {
584        self.get::<ffi::Style>(Data::CURSOR_STYLE)
585            .and_then(std::convert::TryInto::try_into)
586    }
587    /// Get the current Kitty keyboard protocol flags.
588    pub fn kitty_keyboard_flags(&self) -> Result<key::KittyKeyFlags> {
589        self.get::<ffi::KittyKeyFlags>(Data::KITTY_KEYBOARD_FLAGS)
590            .map(key::KittyKeyFlags::from_bits_retain)
591    }
592
593    /// Get the scrollbar state for the terminal viewport.
594    ///
595    /// This may be expensive to calculate depending on where the viewport is
596    /// (arbitrary pins are expensive). The caller should take care to only call
597    /// this as needed and not too frequently.
598    pub fn scrollbar(&self) -> Result<Scrollbar> {
599        self.get(Data::SCROLLBAR)
600    }
601    /// Get the currently active screen.
602    pub fn active_screen(&self) -> Result<Screen> {
603        self.get(Data::ACTIVE_SCREEN)
604    }
605    /// Get whether any mouse tracking mode is active.
606    ///
607    /// Returns true if any of the mouse tracking modes (X1inner, normal, button,
608    /// or any-event) are enabled.
609    pub fn is_mouse_tracking(&self) -> Result<bool> {
610        self.get(Data::MOUSE_TRACKING)
611    }
612    /// Get the terminal title as set by escape sequences (e.g. OSC inner/2).
613    ///
614    /// Returns a borrowed string, valid until the next call to
615    /// [`Terminal::vt_write`] or [`Terminal::reset`]. An empty string is
616    /// returned when no title has been set.
617    pub fn title(&self) -> Result<&str> {
618        let str = self.get::<ffi::String>(Data::TITLE)?;
619        // SAFETY: We trust libghostty to return a valid borrowed string,
620        // while we uphold that no mutation could happen during its lifetime.
621        let str = unsafe { str.to_bytes() };
622        std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
623    }
624
625    /// Get the current working directory as set by escape sequences (e.g. OSC 7).
626    ///
627    /// Returns a borrowed string, valid until the next call to
628    /// [`Terminal::vt_write`] or [`Terminal::reset`]. An empty string is
629    /// returned when no title has been set.
630    pub fn pwd(&self) -> Result<&str> {
631        let str = self.get::<ffi::String>(Data::PWD)?;
632        // SAFETY: We trust libghostty to return a valid borrowed string,
633        // while we uphold that no mutation could happen during its lifetime.
634        let str = unsafe { str.to_bytes() };
635        std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
636    }
637    /// The total number of rows in the active screen including scrollback.
638    pub fn total_rows(&self) -> Result<usize> {
639        self.get(Data::TOTAL_ROWS)
640    }
641    ///  The number of scrollback rows (total rows minus viewport rows).
642    pub fn scrollback_rows(&self) -> Result<usize> {
643        self.get(Data::SCROLLBACK_ROWS)
644    }
645
646    /// The effective foreground color (override or default).
647    pub fn fg_color(&self) -> Result<Option<RgbColor>> {
648        self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND)
649            .map(|v| v.map(Into::into))
650    }
651    /// The default foreground color (ignoring any OSC override).
652    pub fn default_fg_color(&self) -> Result<Option<RgbColor>> {
653        self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND_DEFAULT)
654            .map(|v| v.map(Into::into))
655    }
656    /// Set the default foreground color.
657    pub fn set_default_fg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
658        self.set_optional(Opt::COLOR_FOREGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
659        Ok(self)
660    }
661
662    /// The effective background color (override or default).
663    pub fn bg_color(&self) -> Result<Option<RgbColor>> {
664        self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND)
665            .map(|v| v.map(Into::into))
666    }
667    /// The default background color (ignoring any OSC override).
668    pub fn default_bg_color(&self) -> Result<Option<RgbColor>> {
669        self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND_DEFAULT)
670            .map(|v| v.map(Into::into))
671    }
672    /// Set the default background color.
673    pub fn set_default_bg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
674        self.set_optional(Opt::COLOR_BACKGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
675        Ok(self)
676    }
677
678    /// The effective cursor color (override or default).
679    pub fn cursor_color(&self) -> Result<Option<RgbColor>> {
680        self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR)
681            .map(|v| v.map(Into::into))
682    }
683    /// The default cursor color (ignoring any OSC override).
684    pub fn default_cursor_color(&self) -> Result<Option<RgbColor>> {
685        self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR_DEFAULT)
686            .map(|v| v.map(Into::into))
687    }
688    /// Set the default cursor color.
689    pub fn set_default_cursor_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
690        self.set_optional(Opt::COLOR_CURSOR, v.map(ffi::ColorRgb::from).as_ref())?;
691        Ok(self)
692    }
693
694    /// Set the default cursor style used by DECSCUSR reset (CSI 0 q).
695    ///
696    /// Passing `None` resets to libghostty's built-in block cursor default.
697    pub fn set_default_cursor_style(&mut self, v: Option<CursorStyle>) -> Result<&mut Self> {
698        self.set_optional(Opt::DEFAULT_CURSOR_STYLE, v.as_ref())?;
699        Ok(self)
700    }
701
702    /// Set whether the default cursor blinks when reset by DECSCUSR (CSI 0 q).
703    ///
704    /// Passing `None` resets to libghostty's built-in non-blinking default.
705    pub fn set_default_cursor_blink(&mut self, v: Option<bool>) -> Result<&mut Self> {
706        self.set_optional(Opt::DEFAULT_CURSOR_BLINK, v.as_ref())?;
707        Ok(self)
708    }
709
710    /// The current 256-color palette.
711    pub fn color_palette(&self) -> Result<Palette> {
712        self.get::<RawPalette>(Data::COLOR_PALETTE)
713            .map(Palette::from)
714    }
715    /// The default 256-color palette (ignoring any OSC overrides).
716    pub fn default_color_palette(&self) -> Result<Palette> {
717        self.get::<RawPalette>(Data::COLOR_PALETTE_DEFAULT)
718            .map(Palette::from)
719    }
720    /// Set the default 256-color palette.
721    pub fn set_default_color_palette(&mut self, v: Option<Palette>) -> Result<&mut Self> {
722        self.set_optional::<RawPalette>(Opt::COLOR_PALETTE, v.map(|v| v.into()).as_ref())?;
723        Ok(self)
724    }
725
726    /// Set the maximum bytes the APC handler will buffer for all protocols.
727    ///
728    /// This prevents malicious input from causing unbounded memory allocation.
729    /// A `None` value removes all overrides, reverting to the built-in defaults.
730    pub fn set_apc_max_bytes(&mut self, max: Option<usize>) -> Result<&mut Self> {
731        self.set_optional(ffi::TerminalOption::APC_MAX_BYTES, max.as_ref())?;
732        Ok(self)
733    }
734
735    /// Enable or disable Glyph Protocol APC handling.
736    ///
737    /// Disabling the protocol makes the terminal ignore Glyph Protocol APC
738    /// sequences and clears the session's glyph glossary.
739    pub fn set_glyph_protocol_enabled(&mut self, enabled: bool) -> Result<&mut Self> {
740        self.set(ffi::TerminalOption::GLYPH_PROTOCOL, &enabled)?;
741        Ok(self)
742    }
743}
744impl Drop for Terminal<'_, '_> {
745    fn drop(&mut self) {
746        unsafe { ffi::ghostty_terminal_free(self.inner.as_raw()) }
747    }
748}
749
750/// A point in the terminal grid.
751#[derive(Clone, Copy, Debug, PartialEq, Eq)]
752pub enum Point {
753    /// Active area where the cursor can move.
754    Active(PointCoordinate),
755    /// Visible viewport (changes when scrolled).
756    Viewport(PointCoordinate),
757    /// Full screen including scrollback.
758    Screen(PointCoordinate),
759    /// Scrollback history only (before active area).
760    History(PointCoordinate),
761}
762
763impl From<Point> for ffi::Point {
764    fn from(value: Point) -> Self {
765        match value {
766            Point::Active(coord) => Self {
767                tag: ffi::PointTag::ACTIVE,
768                value: ffi::PointValue {
769                    coordinate: coord.into(),
770                },
771            },
772            Point::Viewport(coord) => Self {
773                tag: ffi::PointTag::VIEWPORT,
774                value: ffi::PointValue {
775                    coordinate: coord.into(),
776                },
777            },
778            Point::Screen(coord) => Self {
779                tag: ffi::PointTag::SCREEN,
780                value: ffi::PointValue {
781                    coordinate: coord.into(),
782                },
783            },
784            Point::History(coord) => Self {
785                tag: ffi::PointTag::HISTORY,
786                value: ffi::PointValue {
787                    coordinate: coord.into(),
788                },
789            },
790        }
791    }
792}
793
794/// A coordinate space for converting grid references back to points.
795#[derive(Clone, Copy, Debug, PartialEq, Eq)]
796pub enum PointSpace {
797    /// Active area where the cursor can move.
798    Active,
799    /// Visible viewport, which changes when scrolled.
800    Viewport,
801    /// Full screen including scrollback.
802    Screen,
803    /// Scrollback history only, before the active area.
804    History,
805}
806
807impl PointSpace {
808    pub(crate) fn into_raw(self) -> ffi::PointTag::Type {
809        match self {
810            Self::Active => ffi::PointTag::ACTIVE,
811            Self::Viewport => ffi::PointTag::VIEWPORT,
812            Self::Screen => ffi::PointTag::SCREEN,
813            Self::History => ffi::PointTag::HISTORY,
814        }
815    }
816}
817
818/// A coordinate in the terminal grid.
819#[derive(Clone, Copy, Debug, PartialEq, Eq)]
820pub struct PointCoordinate {
821    /// Column (0-indexed).
822    pub x: u16,
823    /// Row (0-indexed). May exceed page size for screen/history tags.
824    pub y: u32,
825}
826impl From<PointCoordinate> for ffi::PointCoordinate {
827    fn from(value: PointCoordinate) -> Self {
828        let PointCoordinate { x, y } = value;
829        Self { x, y }
830    }
831}
832impl From<ffi::PointCoordinate> for PointCoordinate {
833    fn from(value: ffi::PointCoordinate) -> Self {
834        let ffi::PointCoordinate { x, y } = value;
835        Self { x, y }
836    }
837}
838
839/// Scroll viewport behavior.
840#[derive(Clone, Copy, Debug, PartialEq, Eq)]
841pub enum ScrollViewport {
842    /// Scroll to the top of the scrollback.
843    Top,
844    /// Scroll to the bottom (active area).
845    Bottom,
846    /// Scroll by a delta amount (up is negative).
847    Delta(isize),
848    /// Scroll to an absolute row offset from the top of the scrollback.
849    Row(usize),
850}
851impl From<ScrollViewport> for ffi::TerminalScrollViewport {
852    fn from(value: ScrollViewport) -> Self {
853        match value {
854            ScrollViewport::Top => Self {
855                tag: ffi::TerminalScrollViewportTag::TOP,
856                value: ffi::TerminalScrollViewportValue::default(),
857            },
858            ScrollViewport::Bottom => Self {
859                tag: ffi::TerminalScrollViewportTag::BOTTOM,
860                value: ffi::TerminalScrollViewportValue::default(),
861            },
862            ScrollViewport::Delta(delta) => Self {
863                tag: ffi::TerminalScrollViewportTag::DELTA,
864                value: {
865                    let mut v = ffi::TerminalScrollViewportValue::default();
866                    v.delta = delta;
867                    v
868                },
869            },
870            ScrollViewport::Row(row) => Self {
871                tag: ffi::TerminalScrollViewportTag::ROW,
872                value: {
873                    let mut v = ffi::TerminalScrollViewportValue::default();
874                    v.row = row;
875                    v
876                },
877            },
878        }
879    }
880}
881
882/// A terminal mode consisting of its value and its kind (DEC/ANSI).
883#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
884pub struct Mode(pub ffi::Mode);
885
886impl Mode {
887    #![expect(missing_docs, reason = "no upstream documentation provided")]
888    const ANSI_BIT: u16 = 1 << 15;
889
890    /// Create a new mode from its numeric value and its kind.
891    #[must_use]
892    pub const fn new(v: u16, kind: ModeKind) -> Self {
893        match kind {
894            ModeKind::Ansi => Self(v | Self::ANSI_BIT),
895            ModeKind::Dec => Self(v),
896        }
897    }
898
899    /// The numeric value of the mode.
900    #[must_use]
901    pub const fn value(self) -> u16 {
902        (self.0) & 0x7fff
903    }
904
905    /// The kind of the mode (DEC/ANSI).
906    #[must_use]
907    pub const fn kind(self) -> ModeKind {
908        if (self.0) & Self::ANSI_BIT > 0 {
909            ModeKind::Ansi
910        } else {
911            ModeKind::Dec
912        }
913    }
914
915    pub const KAM: Self = Self::new(2, ModeKind::Ansi);
916    pub const INSERT: Self = Self::new(4, ModeKind::Ansi);
917    pub const SRM: Self = Self::new(12, ModeKind::Ansi);
918    pub const LINEFEED: Self = Self::new(20, ModeKind::Ansi);
919
920    pub const DECCKM: Self = Self::new(1, ModeKind::Dec);
921    pub const _132_COLUMN: Self = Self::new(3, ModeKind::Dec);
922    pub const SLOW_SCROLL: Self = Self::new(4, ModeKind::Dec);
923    pub const REVERSE_COLORS: Self = Self::new(5, ModeKind::Dec);
924    pub const ORIGIN: Self = Self::new(6, ModeKind::Dec);
925    pub const WRAPAROUND: Self = Self::new(7, ModeKind::Dec);
926    pub const AUTOREPEAT: Self = Self::new(8, ModeKind::Dec);
927    pub const X10_MOUSE: Self = Self::new(9, ModeKind::Dec);
928    pub const CURSOR_BLINKING: Self = Self::new(12, ModeKind::Dec);
929    pub const CURSOR_VISIBLE: Self = Self::new(25, ModeKind::Dec);
930    pub const ENABLE_MODE3: Self = Self::new(40, ModeKind::Dec);
931    pub const REVERSE_WRAP: Self = Self::new(45, ModeKind::Dec);
932    pub const ALT_SCREEN_LEGACY: Self = Self::new(47, ModeKind::Dec);
933    pub const KEYPAD_KEYS: Self = Self::new(66, ModeKind::Dec);
934    pub const LEFT_RIGHT_MARGIN: Self = Self::new(69, ModeKind::Dec);
935    pub const NORMAL_MOUSE: Self = Self::new(1000, ModeKind::Dec);
936    pub const BUTTON_MOUSE: Self = Self::new(1002, ModeKind::Dec);
937    pub const ANY_MOUSE: Self = Self::new(1003, ModeKind::Dec);
938    pub const FOCUS_EVENT: Self = Self::new(1004, ModeKind::Dec);
939    pub const UTF8_MOUSE: Self = Self::new(1005, ModeKind::Dec);
940    pub const SGR_MOUSE: Self = Self::new(1006, ModeKind::Dec);
941    pub const ALT_SCROLL: Self = Self::new(1007, ModeKind::Dec);
942    pub const URXVT_MOUSE: Self = Self::new(1015, ModeKind::Dec);
943    pub const SGR_PIXELS_MOUSE: Self = Self::new(1016, ModeKind::Dec);
944    pub const NUMLOCK_KEYPAD: Self = Self::new(1035, ModeKind::Dec);
945    pub const ALT_ESC_PREFIX: Self = Self::new(1036, ModeKind::Dec);
946    pub const ALT_SENDS_ESC: Self = Self::new(1039, ModeKind::Dec);
947    pub const REVERSE_WRAP_EXT: Self = Self::new(1045, ModeKind::Dec);
948    pub const ALT_SCREEN: Self = Self::new(1047, ModeKind::Dec);
949    pub const SAVE_CURSOR: Self = Self::new(1048, ModeKind::Dec);
950    pub const ALT_SCREEN_SAVE: Self = Self::new(1049, ModeKind::Dec);
951    pub const BRACKETED_PASTE: Self = Self::new(2004, ModeKind::Dec);
952    pub const SYNC_OUTPUT: Self = Self::new(2026, ModeKind::Dec);
953    pub const GRAPHEME_CLUSTER: Self = Self::new(2027, ModeKind::Dec);
954    pub const COLOR_SCHEME_REPORT: Self = Self::new(2031, ModeKind::Dec);
955    pub const IN_BAND_RESIZE: Self = Self::new(2048, ModeKind::Dec);
956}
957
958/// The kind of a terminal mode.
959#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
960pub enum ModeKind {
961    /// DEC terminal mode.
962    Dec,
963    /// ANSI terminal mode.
964    Ansi,
965}
966
967impl From<Mode> for ffi::Mode {
968    fn from(value: Mode) -> Self {
969        value.0
970    }
971}
972
973/// Device attributes response data for all three DA levels.
974/// Filled by the [`Terminal::on_device_attributes`] callback in response
975/// to CSI c, CSI > c, or CSI = c queries. The terminal uses whichever
976/// sub-struct matches the request type.
977#[derive(Debug, Clone, Copy)]
978pub struct DeviceAttributes {
979    /// Primary device attributes (DA1).
980    pub primary: PrimaryDeviceAttributes,
981    /// Secondary device attributes (DA2).
982    pub secondary: SecondaryDeviceAttributes,
983    /// Tertiary device attributes (DA3).
984    pub tertiary: TertiaryDeviceAttributes,
985}
986
987impl From<DeviceAttributes> for ffi::DeviceAttributes {
988    fn from(value: DeviceAttributes) -> Self {
989        Self {
990            primary: value.primary.into(),
991            secondary: value.secondary.into(),
992            tertiary: value.tertiary.into(),
993        }
994    }
995}
996
997/// Primary device attributes (DA1) response data.
998///
999/// Returned as part of [`DeviceAttributes`] in response to a CSI c query.
1000#[derive(Debug, Clone, Copy)]
1001pub struct PrimaryDeviceAttributes(ffi::DeviceAttributesPrimary);
1002
1003impl PrimaryDeviceAttributes {
1004    /// Construct primary device attributes from a conformance level
1005    /// and an array of device attribute features.
1006    ///
1007    /// Prefer defining primary device attributes as a `const` when the feature
1008    /// list is statically known. That makes the 64-feature limit fail during
1009    /// compilation instead of panicking at runtime.
1010    ///
1011    /// # Panics
1012    ///
1013    /// **Panics** when more than 64 features are given.
1014    #[must_use]
1015    pub const fn new(
1016        conformance_level: ConformanceLevel,
1017        features: &[DeviceAttributeFeature],
1018    ) -> Self {
1019        assert!(features.len() <= 64);
1020
1021        let mut f = [0u16; 64];
1022        let mut i = 0;
1023        while i < features.len() {
1024            f[i] = features[i].0;
1025            i += 1;
1026        }
1027
1028        Self(ffi::DeviceAttributesPrimary {
1029            conformance_level: conformance_level.0,
1030            features: f,
1031            num_features: features.len(),
1032        })
1033    }
1034}
1035
1036impl From<PrimaryDeviceAttributes> for ffi::DeviceAttributesPrimary {
1037    fn from(value: PrimaryDeviceAttributes) -> Self {
1038        value.0
1039    }
1040}
1041
1042/// The level of conformance to the behavior of a specific or a family of
1043/// physical terminal models.
1044#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1045pub struct ConformanceLevel(pub u16);
1046
1047impl ConformanceLevel {
1048    #![expect(clippy::doc_markdown, reason = "false positive")]
1049    #![expect(missing_docs, reason = "self-explanatory")]
1050    pub const VT100: Self = Self(ffi::DA_CONFORMANCE_VT100);
1051    pub const VT101: Self = Self(ffi::DA_CONFORMANCE_VT101);
1052    pub const VT102: Self = Self(ffi::DA_CONFORMANCE_VT102);
1053    pub const VT125: Self = Self(ffi::DA_CONFORMANCE_VT125);
1054    pub const VT131: Self = Self(ffi::DA_CONFORMANCE_VT131);
1055    pub const VT132: Self = Self(ffi::DA_CONFORMANCE_VT132);
1056    pub const VT220: Self = Self(ffi::DA_CONFORMANCE_VT220);
1057    pub const VT240: Self = Self(ffi::DA_CONFORMANCE_VT240);
1058    pub const VT320: Self = Self(ffi::DA_CONFORMANCE_VT320);
1059    pub const VT340: Self = Self(ffi::DA_CONFORMANCE_VT340);
1060    pub const VT420: Self = Self(ffi::DA_CONFORMANCE_VT420);
1061    pub const VT510: Self = Self(ffi::DA_CONFORMANCE_VT510);
1062    pub const VT520: Self = Self(ffi::DA_CONFORMANCE_VT520);
1063    pub const VT525: Self = Self(ffi::DA_CONFORMANCE_VT525);
1064    /// Equivalent to a VT2xx terminal.
1065    pub const LEVEL_2: Self = Self(ffi::DA_CONFORMANCE_LEVEL_2);
1066    /// Equivalent to a VT3xx terminal.
1067    pub const LEVEL_3: Self = Self(ffi::DA_CONFORMANCE_LEVEL_3);
1068    /// Equivalent to a VT4xx terminal.
1069    pub const LEVEL_4: Self = Self(ffi::DA_CONFORMANCE_LEVEL_4);
1070    /// Equivalent to a VT5xx terminal.
1071    pub const LEVEL_5: Self = Self(ffi::DA_CONFORMANCE_LEVEL_5);
1072}
1073
1074/// A feature that a terminal can report to support.
1075#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1076pub struct DeviceAttributeFeature(pub u16);
1077
1078impl DeviceAttributeFeature {
1079    #![expect(missing_docs, reason = "no upstream documentation provided")]
1080    pub const COLUMNS_132: Self = Self(ffi::DA_FEATURE_COLUMNS_132);
1081    pub const PRINTER: Self = Self(ffi::DA_FEATURE_PRINTER);
1082    pub const REGIS: Self = Self(ffi::DA_FEATURE_REGIS);
1083    pub const SIXEL: Self = Self(ffi::DA_FEATURE_SIXEL);
1084    pub const SELECTIVE_ERASE: Self = Self(ffi::DA_FEATURE_SELECTIVE_ERASE);
1085    pub const USER_DEFINED_KEYS: Self = Self(ffi::DA_FEATURE_USER_DEFINED_KEYS);
1086    pub const NATIONAL_REPLACEMENT: Self = Self(ffi::DA_FEATURE_NATIONAL_REPLACEMENT);
1087    pub const TECHNICAL_CHARACTERS: Self = Self(ffi::DA_FEATURE_TECHNICAL_CHARACTERS);
1088    pub const LOCATOR: Self = Self(ffi::DA_FEATURE_LOCATOR);
1089    pub const TERMINAL_STATE: Self = Self(ffi::DA_FEATURE_TERMINAL_STATE);
1090    pub const WINDOWING: Self = Self(ffi::DA_FEATURE_WINDOWING);
1091    pub const HORIZONTAL_SCROLLING: Self = Self(ffi::DA_FEATURE_HORIZONTAL_SCROLLING);
1092    pub const ANSI_COLOR: Self = Self(ffi::DA_FEATURE_ANSI_COLOR);
1093    pub const RECTANGULAR_EDITING: Self = Self(ffi::DA_FEATURE_RECTANGULAR_EDITING);
1094    pub const ANSI_TEXT_LOCATOR: Self = Self(ffi::DA_FEATURE_ANSI_TEXT_LOCATOR);
1095    pub const CLIPBOARD: Self = Self(ffi::DA_FEATURE_CLIPBOARD);
1096}
1097
1098/// Secondary device attributes (DA2) response data.
1099///
1100/// Returned as part of [`DeviceAttributes`] in response to a CSI > c query.
1101/// Response format: CSI > Pp ; Pv ; Pc c
1102#[derive(Debug, Copy, Clone)]
1103pub struct SecondaryDeviceAttributes {
1104    /// Terminal type identifier (Pp).
1105    pub device_type: DeviceType,
1106    /// Firmware/patch version number (Pv).
1107    pub firmware_version: u16,
1108    /// ROM cartridge registration number (Pc). Always 0 for emulators.
1109    pub rom_cartridge: u16,
1110}
1111
1112impl From<SecondaryDeviceAttributes> for ffi::DeviceAttributesSecondary {
1113    fn from(value: SecondaryDeviceAttributes) -> Self {
1114        Self {
1115            device_type: value.device_type.0,
1116            firmware_version: value.firmware_version,
1117            rom_cartridge: value.rom_cartridge,
1118        }
1119    }
1120}
1121
1122/// The type of terminal device being emulated.
1123#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1124pub struct DeviceType(pub u16);
1125
1126impl DeviceType {
1127    #![expect(missing_docs, reason = "self-explanatory")]
1128    pub const VT100: Self = Self(ffi::DA_DEVICE_TYPE_VT100);
1129    pub const VT220: Self = Self(ffi::DA_DEVICE_TYPE_VT220);
1130    pub const VT240: Self = Self(ffi::DA_DEVICE_TYPE_VT240);
1131    pub const VT330: Self = Self(ffi::DA_DEVICE_TYPE_VT330);
1132    pub const VT340: Self = Self(ffi::DA_DEVICE_TYPE_VT340);
1133    pub const VT320: Self = Self(ffi::DA_DEVICE_TYPE_VT320);
1134    pub const VT382: Self = Self(ffi::DA_DEVICE_TYPE_VT382);
1135    pub const VT420: Self = Self(ffi::DA_DEVICE_TYPE_VT420);
1136    pub const VT510: Self = Self(ffi::DA_DEVICE_TYPE_VT510);
1137    pub const VT520: Self = Self(ffi::DA_DEVICE_TYPE_VT520);
1138    pub const VT525: Self = Self(ffi::DA_DEVICE_TYPE_VT525);
1139}
1140
1141/// Tertiary device attributes (DA3) response data.
1142///
1143/// Returned as part of [`DeviceAttributes`] in response to a CSI = c query.
1144/// Response format: DCS ! | D...D ST (DECRPTUI).
1145#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1146pub struct TertiaryDeviceAttributes {
1147    /// Unit ID encoded as 8 uppercase hex digits in the response.
1148    pub unit_id: u32,
1149}
1150
1151impl From<TertiaryDeviceAttributes> for ffi::DeviceAttributesTertiary {
1152    fn from(value: TertiaryDeviceAttributes) -> Self {
1153        Self {
1154            unit_id: value.unit_id,
1155        }
1156    }
1157}
1158
1159/// Color scheme reported in response to a CSI ? 996 n query.
1160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1161#[repr(u32)]
1162#[expect(missing_docs, reason = "self-explanatory")]
1163pub enum ColorScheme {
1164    Light = ffi::ColorScheme::LIGHT,
1165    Dark = ffi::ColorScheme::DARK,
1166}
1167
1168impl ColorScheme {
1169    /// Encode a color scheme report into an escape sequence.
1170    ///
1171    /// Encodes a color scheme report into the provided buffer. Dark color
1172    /// schemes emit `ESC [ ? 997 ; 1 n`, and light color schemes emit
1173    /// `ESC [ ? 997 ; 2 n`. The encoded bytes are identical to the terminal's
1174    /// internal `CSI ? 996 n` query response.
1175    ///
1176    /// Hosts should gate unsolicited sends on mode 2031 being set, which can
1177    /// be checked via the mode getters.
1178    ///
1179    /// If the buffer is too small, returns [`Error::OutOfSpace`] with the
1180    /// required buffer size. The caller can then retry with a sufficiently
1181    /// sized buffer.
1182    pub fn encode_report(self, buf: &mut [u8]) -> Result<usize> {
1183        let mut written = 0;
1184        let result = unsafe {
1185            ffi::ghostty_color_scheme_report_encode(
1186                self.into(),
1187                buf.as_mut_ptr().cast(),
1188                buf.len(),
1189                &raw mut written,
1190            )
1191        };
1192        from_result_with_len(result, written)
1193    }
1194}
1195
1196impl From<ColorScheme> for ffi::ColorScheme::Type {
1197    fn from(value: ColorScheme) -> Self {
1198        match value {
1199            ColorScheme::Light => ffi::ColorScheme::LIGHT,
1200            ColorScheme::Dark => ffi::ColorScheme::DARK,
1201        }
1202    }
1203}
1204
1205/// Amount of compression work to perform before returning.
1206#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
1207#[repr(u32)]
1208pub enum CompressionMode {
1209    /// Perform one bounded compression step suitable for idle scheduling.
1210    Incremental = ffi::TerminalCompressionMode::INCREMENTAL,
1211    /// Synchronously inspect every currently eligible page.
1212    Full = ffi::TerminalCompressionMode::FULL,
1213}
1214
1215/// Scheduling result from terminal compression.
1216#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
1217#[repr(u32)]
1218pub enum CompressionResult {
1219    /// Retained-mapping reclamation is unavailable on this target.
1220    Unsupported = ffi::TerminalCompressionResult::UNSUPPORTED,
1221    /// More incremental compression work remains.
1222    Pending = ffi::TerminalCompressionResult::PENDING,
1223    /// The pass has no continuation to schedule.
1224    Complete = ffi::TerminalCompressionResult::COMPLETE,
1225}
1226
1227/// Opaque token representing a terminal's current compression activity.
1228///
1229/// The token is opaque and only equality comparisons are meaningful.
1230/// An embedding application should cache it and restart its compression idle
1231/// delay whenever the value changes. The value may wrap and changes in either
1232/// direction have the same meaning.
1233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1234pub struct CompressionActivity(u64);
1235
1236/// A semantic, atomic clipboard write.
1237///
1238/// The request, contents array, MIME strings, and data strings are all
1239/// borrowed and valid only for the callback duration.
1240#[derive(Clone, Debug)]
1241pub struct ClipboardWrite<'t> {
1242    ptr: *const ffi::ClipboardWrite,
1243    _phan: PhantomData<&'t ()>,
1244}
1245
1246impl<'t> ClipboardWrite<'t> {
1247    /// # Safety
1248    ///
1249    /// Caller must ensure that the given pointer has the correct lifetime.
1250    unsafe fn from_raw(ptr: *const ffi::ClipboardWrite) -> Self {
1251        Self {
1252            ptr,
1253            _phan: PhantomData,
1254        }
1255    }
1256
1257    /// Get the clipboard's destination.
1258    pub fn location(&self) -> ClipboardLocation {
1259        // SAFETY: We trust libghostty to give us a valid pointer
1260        // within the lifetime of the callback.
1261        unsafe { *self.ptr }
1262            .location
1263            .try_into()
1264            .unwrap_or(ClipboardLocation::Standard)
1265    }
1266    /// Get an iterator into a borrowed array of MIME representations.
1267    ///
1268    /// The iterator is empty for a write carrying no representations, which
1269    /// requests that the destination be cleared (e.g. OSC 52 with an empty
1270    /// payload).
1271    pub fn contents(&self) -> ClipboardContents<'t> {
1272        // SAFETY: We trust libghostty to give us a valid pointer
1273        // within the lifetime of the callback.
1274        let raw = unsafe { *self.ptr };
1275        // The C API declares `contents` optional and sends null for a write
1276        // carrying no representations (the "clear the clipboard" shape);
1277        // `from_raw_parts` requires a non-null pointer even at length zero.
1278        let contents: &'t [ffi::ClipboardContent] = if raw.contents.is_null() {
1279            &[]
1280        } else {
1281            // SAFETY: We trust libghostty to give us a valid pointer and
1282            // length within the lifetime of the callback.
1283            unsafe { std::slice::from_raw_parts(raw.contents, raw.contents_len) }
1284        };
1285        ClipboardContents(contents.iter())
1286    }
1287}
1288
1289/// An iterator into a borrowed array of MIME representations.
1290#[derive(Clone, Debug)]
1291pub struct ClipboardContents<'t>(std::slice::Iter<'t, ffi::ClipboardContent>);
1292
1293impl<'t> Iterator for ClipboardContents<'t> {
1294    type Item = ClipboardContent<'t>;
1295
1296    fn next(&mut self) -> Option<Self::Item> {
1297        self.0
1298            .next()
1299            .map(|v| unsafe { ClipboardContent::from_raw(v) })
1300    }
1301    fn size_hint(&self) -> (usize, Option<usize>) {
1302        self.0.size_hint()
1303    }
1304}
1305impl DoubleEndedIterator for ClipboardContents<'_> {
1306    fn next_back(&mut self) -> Option<Self::Item> {
1307        self.0
1308            .next_back()
1309            .map(|v| unsafe { ClipboardContent::from_raw(v) })
1310    }
1311}
1312impl ExactSizeIterator for ClipboardContents<'_> {}
1313impl std::iter::FusedIterator for ClipboardContents<'_> {}
1314
1315/// One MIME representation in a clipboard write.
1316///
1317/// The data is binary-safe and has already been decoded from any protocol-level
1318/// encoding. A zero-length data string is an explicit empty representation; it
1319/// does not clear the clipboard.
1320#[derive(Clone, Copy, Debug)]
1321pub struct ClipboardContent<'t> {
1322    /// MIME type of the representation.
1323    pub mime: &'t str,
1324    /// Decoded representation data.
1325    ///
1326    /// The C API delivers binary-safe bytes, but this field is `&str` for
1327    /// compatibility within 0.2.x, so data that is not valid UTF-8 (e.g. an
1328    /// `image/png` representation) is exposed as an empty string. 0.3.0
1329    /// changes this field to `&[u8]`.
1330    pub data: &'t str,
1331}
1332impl<'t> ClipboardContent<'t> {
1333    /// # Safety
1334    ///
1335    /// Caller must guarantee that the given raw value is valid within
1336    /// the given lifetime.
1337    unsafe fn from_raw(value: &ffi::ClipboardContent) -> Self {
1338        // SAFETY: Upheld by caller
1339        unsafe {
1340            Self {
1341                // Ghostty currently only emits ASCII mime types, but the C
1342                // API does not guarantee UTF-8, so validate rather than
1343                // trust; fall back to the opaque-bytes mime type.
1344                mime: std::str::from_utf8(value.mime.to_bytes())
1345                    .unwrap_or("application/octet-stream"),
1346                // The data is binary-safe per the C API, so it must be
1347                // validated before being exposed as `str`. Changing the field
1348                // to `&[u8]` would break 0.2.x, so non-UTF-8 data degrades to
1349                // an empty string instead of producing an invalid `&str`.
1350                data: std::str::from_utf8(value.data.to_bytes()).unwrap_or(""),
1351            }
1352        }
1353    }
1354}
1355
1356/// Clipboard destination for a clipboard write.
1357///
1358/// Protocol-specific destination identifiers are normalized to these values
1359/// before the clipboard write callback is invoked.
1360#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
1361#[repr(u32)]
1362pub enum ClipboardLocation {
1363    /// The standard system clipboard.
1364    Standard = ffi::ClipboardLocation::STANDARD,
1365    /// The selection clipboard.
1366    Selection = ffi::ClipboardLocation::SELECTION,
1367    /// The primary selection clipboard.
1368    Primary = ffi::ClipboardLocation::PRIMARY,
1369}
1370
1371/// Possible errors caused by a clipboard write callback.
1372///
1373/// Protocols without write acknowledgements, including OSC 52 and iTerm2
1374/// OSC 1337 Copy, ignore this result.
1375#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
1376#[repr(u32)]
1377pub enum ClipboardWriteError {
1378    /// The clipboard write was denied by policy or the user.
1379    Denied = ffi::ClipboardWriteResult::DENIED,
1380    /// The destination or one or more representations are unsupported.
1381    Unsupported = ffi::ClipboardWriteResult::UNSUPPORTED,
1382    /// The clipboard is temporarily unavailable.
1383    Busy = ffi::ClipboardWriteResult::BUSY,
1384    /// One or more representations contain invalid data.
1385    InvalidData = ffi::ClipboardWriteResult::INVALID_DATA,
1386    /// The clipboard write failed due to an I/O error.
1387    IoError = ffi::ClipboardWriteResult::IO_ERROR,
1388}
1389
1390//---------------------------------------
1391// Callbacks
1392//---------------------------------------
1393
1394/// You might be wondering just what the heck this is.
1395///
1396/// Truth to be told, you don't need to understand how it works
1397/// in order to use it. It does a bunch of voodoo behind the scenes
1398/// that make sure all the invariants of the C API are upheld, while
1399/// providing a convenient API for Rust users.
1400///
1401/// Each handler is defined in this following format:
1402/// ```ignore
1403/// pub fn on_foobar(
1404///     &mut self,
1405///     // The corresponding GhosttyTerminalOption
1406///     tag = FOOBAR,
1407///
1408///     // The name of the original function type in C,
1409///     // along with the extra C parameters and the expected C return type
1410///     from = GhosttyTerminalFoobarFn(foo: *const u8, bar: usize) -> bool,
1411///
1412///     // The name of mapped Rust function type,
1413///     // along with the Rust parameters and return type.
1414///     //
1415///     // `<'t>` is used to tie the return value to the lifetime of the
1416///     // terminal. The name is arbitrary - any lifetime marker will do.
1417///     to = <'t>FoobarFn(&'t [u8]) -> bool,
1418/// ) |term, func| {
1419///     // `term` is the terminal and `func` is the Rust callback.
1420///     // Both names are arbitrary.
1421///
1422///     // Convert the raw parameters into Rust types.
1423///     // This is just to illustrate how.
1424///     let slice = unsafe { std::slice::from_raw_parts(foo, bar) };
1425///
1426///     // Call into user logic and return.
1427///     func(&terminal, slice)
1428/// }
1429/// ```
1430macro_rules! handlers {
1431    {
1432        $(
1433            $(#[$fmeta:meta])*
1434            $vis:vis fn $name:ident(
1435                &mut self,
1436                tag = $tag:ident,
1437                from = $rawfnty:ident( $($rfname:ident: $rfty:ty),*$(,)? ) $(-> $rawrty:ty)?,
1438                $(#[$tmeta:meta])*
1439                to = $(<$lf:lifetime>)? $fnty:ident( $($fty:ty),*$(,)? ) $(-> $rty:ty)?,
1440            ) |$t:ident, $func:ident| $block:block
1441        )*
1442    } => {
1443        /// Methods for registering [effect handlers](#effects).
1444        impl<'alloc, 'cb> $crate::terminal::Terminal<'alloc, 'cb> {$(
1445            $(#[$fmeta])*
1446            ///
1447            /// See [#Effects](Terminal#effects) for more details.
1448            $vis fn $name(&mut self, f: impl $fnty<'alloc, 'cb>) -> $crate::error::Result<&mut Self> {
1449                unsafe extern "C" fn callback(
1450                    t: $crate::ffi::Terminal,
1451                    ud: *mut std::ffi::c_void,
1452                    $($rfname: $rfty),*
1453                ) $(-> $rawrty)? {
1454                    // SAFETY: USERDATA is set to the boxed VTable pointee
1455                    // (derived from a mutable reference for write provenance)
1456                    // before the callback is registered. ghostty invokes
1457                    // callbacks synchronously during vt_write, so the VTable
1458                    // remains alive and exclusively accessed for the duration
1459                    // of this call.
1460                    let vtable = unsafe { &mut *ud.cast::<VTable<'_, '_>>() };
1461
1462                    let obj = $crate::alloc::Object::new(t).expect("received null terminal ptr in callback - this is a bug!");
1463                    // Build a temporary borrowed Terminal view for the callback
1464                    // without taking ownership of the underlying ghostty terminal.
1465                    let mut term = ::core::mem::ManuallyDrop::new($crate::terminal::Terminal::<'_, '_> {
1466                        inner: obj,
1467                        vtable: ::core::default::Default::default(),
1468                    });
1469                    let $t: &$crate::terminal::Terminal = &term;
1470                    let $func = vtable.$name.as_deref_mut()
1471                        .expect("no handler set but callback is still called - this is a bug!");
1472                    let ret = $block;
1473
1474                    // SAFETY: The temporary vtable was allocated solely to satisfy
1475                    // the Terminal layout expected by the callback signature. Drop
1476                    // it explicitly while intentionally leaving the borrowed
1477                    // terminal handle itself untouched.
1478                    unsafe { ::core::ptr::drop_in_place(&mut term.vtable) };
1479
1480                    ret
1481                }
1482
1483                self.vtable.$name = Some(::std::boxed::Box::new(f));
1484
1485                // USERDATA is a raw pointer option: pass the heap allocation
1486                // itself, not the address of the Box smart pointer field stored
1487                // inline in Terminal.
1488                //
1489                // Derive the pointer from a mutable reference so it carries
1490                // write provenance – the callback later reborrows it as &mut.
1491                let userdata = std::ptr::from_mut::<VTable<'alloc, 'cb>>(self.vtable.as_mut())
1492                    as *const ::std::ffi::c_void;
1493                self.set_ptr($crate::ffi::TerminalOption::USERDATA, userdata)?;
1494
1495                // The callback must be coerced into a function *pointer*
1496                // and not a function *item* (which is a ZST whose address is meaningless).
1497                // :)
1498                let callback_ptr: unsafe extern "C" fn(
1499                    $crate::ffi::Terminal,
1500                    *mut ::std::ffi::c_void,
1501                    $($rfty),*
1502                ) $(-> $rawrty)? = callback;
1503
1504                let result = unsafe {
1505                    $crate::ffi::ghostty_terminal_set(
1506                        self.inner.as_raw(),
1507                        $crate::ffi::TerminalOption::$tag,
1508                        callback_ptr as *const ::std::ffi::c_void
1509                    )
1510                };
1511                $crate::error::from_result(result)?;
1512                Ok(self)
1513            }
1514        )*}
1515        $(
1516            #[doc = concat!(
1517                "[Effect](Terminal#effects) callback type for [`Terminal::",
1518                stringify!($name),
1519                "`](Terminal::",
1520                stringify!($name),
1521                ").\n"
1522            )]
1523            $(#[$tmeta])*
1524            pub trait $fnty<'alloc, 'cb>:
1525                $(for<$lf>)? FnMut(
1526                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1527                    $($fty),*
1528                ) $(-> $rty)? + 'cb {}
1529
1530            impl<'alloc, 'cb, F> $fnty<'alloc, 'cb> for F
1531            where
1532                F: $(for<$lf>)? FnMut(
1533                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1534                    $($fty),*
1535                ) $(-> $rty)? + 'cb
1536            {}
1537        )*
1538
1539        struct VTable<'alloc, 'cb> {
1540            $($name: Option<::std::boxed::Box<dyn $fnty<'alloc, 'cb>>>),*
1541        }
1542
1543        impl ::core::fmt::Debug for VTable<'_, '_> {
1544            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
1545                f.write_str("VTable {..}")
1546            }
1547        }
1548
1549        impl ::core::default::Default for VTable<'_, '_> {
1550            fn default() -> Self {
1551                Self {
1552                    $($name: None),*
1553                }
1554            }
1555        }
1556    };
1557}
1558
1559handlers! {
1560    /// Call the given function when the terminal needs to write data back
1561    /// to the pty (e.g. in response to a DECRQM query or device status report).
1562    pub fn on_pty_write(
1563        &mut self,
1564        tag = WRITE_PTY,
1565        from = GhosttyTerminalWritePtyFn(ptr: *const u8, len: usize),
1566        to = <'t>PtyWriteFn(&'t [u8]),
1567    ) |term, func| {
1568        // SAFETY: We trust libghostty to return valid memory given we
1569        // uphold all lifetime invariants (e.g. no `vt_write` calls
1570        // during this callback, which is guaranteed via the mutable reference).
1571        let data = unsafe { std::slice::from_raw_parts(ptr, len) };
1572        func(&term, data);
1573    }
1574
1575    /// Call the given function when the terminal receives
1576    /// a BEL character (0x07).
1577    pub fn on_bell(
1578        &mut self,
1579        tag = BELL,
1580        from = GhosttyTerminalBellFn(),
1581        to = BellFn(),
1582    ) |term, func| {
1583        func(&term);
1584    }
1585
1586    /// Call the given function when the terminal receives
1587    /// an ENQ character (0x05).
1588    pub fn on_enquiry(
1589        &mut self,
1590        tag = ENQUIRY,
1591        from = GhosttyTerminalEnquiryFn() -> ffi::String,
1592        to = <'t>EnquiryFn() -> Option<&'t str>,
1593    ) |term, func| {
1594        func(&term).unwrap_or("").into()
1595    }
1596
1597    /// Call the given function when the terminal receives an XTVERSION
1598    /// query (CSI > q), and respond with the resulting version string
1599    /// (e.g. "myterm 1.0").
1600    pub fn on_xtversion(
1601        &mut self,
1602        tag = XTVERSION,
1603        from = GhosttyTerminalXtversionFn() -> ffi::String,
1604        to = <'t>XtversionFn() -> Option<&'t str>,
1605    ) |term, func| {
1606        func(&term).unwrap_or("").into()
1607    }
1608
1609    /// Call the given function when the terminal title changes
1610    /// via escape sequences (e.g. OSC 0 or OSC 2).
1611    ///
1612    /// The new title can be queried from the terminal after
1613    /// the callback returns.
1614    pub fn on_title_changed(
1615        &mut self,
1616        tag = TITLE_CHANGED,
1617        from = GhosttyTerminalTitleChangedFn(),
1618        to = TitleChangedFn(),
1619    ) |term, func| {
1620        func(&term);
1621    }
1622
1623    /// Call the given function when the terminal current working directory
1624    /// changes via escape sequences (e.g. OSC 7, OSC 9, or OSC 1337).
1625    ///
1626    /// The new working directory can be queried from the terminal after
1627    /// the callback returns.
1628    pub fn on_pwd_changed(
1629        &mut self,
1630        tag = PWD_CHANGED,
1631        from = GhosttyTerminalPwdChangedFn(),
1632        to = PwdChangedFn(),
1633    ) |term, func| {
1634        func(&term);
1635    }
1636
1637    /// Call the given function in response to XTWINOPS size queries
1638    /// (CSI 14/16/18 t).
1639    pub fn on_size(
1640        &mut self,
1641        tag = SIZE,
1642        from = GhosttyTerminalSizeFn(out: *mut ffi::SizeReportSize) -> bool,
1643        to = SizeFn() -> Option<SizeReportSize>,
1644    ) |term, func| {
1645        if let Some(size) = func(&term) {
1646            // SAFETY: Out pointer is assumed to be valid.
1647            unsafe { *out = size };
1648            true
1649        } else {
1650            false
1651        }
1652    }
1653
1654    /// Call the given function in response to a color scheme
1655    /// device status report query (CSI ? 996 n).
1656    ///
1657    /// Return `Some` to report the current color scheme,
1658    /// or return `None` to silently ignore.
1659    pub fn on_color_scheme(
1660        &mut self,
1661        tag = COLOR_SCHEME,
1662        from = GhosttyTerminalColorSchemeFn(out: *mut ffi::ColorScheme::Type) -> bool,
1663        to = ColorSchemeFn() -> Option<ColorScheme>,
1664    ) |term, func| {
1665        if let Some(size) = func(&term) {
1666            // SAFETY: Out pointer is assumed to be valid.
1667            unsafe { *out = size.into() };
1668            true
1669        } else {
1670            false
1671        }
1672    }
1673
1674    /// Call the given function in response to a device attributes query
1675    /// (CSI c, CSI > c, or CSI = c).
1676    ///
1677    /// Return `Some` with the response data,
1678    /// or return `None` to silently ignore.
1679    pub fn on_device_attributes(
1680        &mut self,
1681        tag = DEVICE_ATTRIBUTES,
1682        from = GhosttyTerminalDeviceAttributesFn(out: *mut ffi::DeviceAttributes) -> bool,
1683        to = DeviceAttributesFn() -> Option<DeviceAttributes>,
1684    ) |term, func| {
1685        if let Some(size) = func(&term) {
1686            // SAFETY: Out pointer is assumed to be valid.
1687            unsafe { *out = size.into() };
1688            true
1689        } else {
1690            false
1691        }
1692    }
1693
1694    /// Call the given function when the running program performs a clipboard write.
1695    ///
1696    ///
1697    /// Protocol details such as OSC 52 selectors, base64 encoding, multipart
1698    /// chunks, aliases, and terminators are normalized before this callback is
1699    /// invoked. OSC 52 and iTerm2 OSC 1337 Copy writes therefore use the same
1700    /// callback shape.
1701    ///
1702    /// OSC 52 clipboard read requests (\"?\") are always ignored and never
1703    /// forwarded to this callback.
1704    pub fn on_clipboard_write(
1705        &mut self,
1706        tag = CLIPBOARD_WRITE,
1707        from = GhosttyTerminalClipboardWriteFn(
1708            write: *const ffi::ClipboardWrite
1709        ) -> ffi::ClipboardWriteResult::Type,
1710        to = <'t>ClipboardWriteFn(ClipboardWrite<'t>) -> std::result::Result<(), ClipboardWriteError>,
1711    ) |term, func| {
1712        match func(&term, unsafe { ClipboardWrite::from_raw(write) }) {
1713            Ok(_) => ffi::ClipboardWriteResult::SUCCESS,
1714            Err(e) => e.into()
1715        }
1716    }
1717}
1718
1719#[cfg(test)]
1720mod tests {
1721    use super::*;
1722    use crate::RenderState;
1723    use crate::render::CursorVisualStyle;
1724    use std::cell::{Cell, RefCell};
1725    use std::mem::ManuallyDrop;
1726
1727    #[inline(never)]
1728    fn build_terminal<'cb>(callback_count: &'cb RefCell<usize>) -> Terminal<'static, 'cb> {
1729        let mut terminal = Terminal::new(Options {
1730            cols: 80,
1731            rows: 24,
1732            max_scrollback: 1000,
1733        })
1734        .expect("terminal should initialize");
1735
1736        terminal
1737            .on_device_attributes(move |_term| {
1738                *callback_count.borrow_mut() += 1;
1739                Some(DeviceAttributes {
1740                    primary: PrimaryDeviceAttributes::new(
1741                        ConformanceLevel::VT220,
1742                        &[DeviceAttributeFeature::ANSI_COLOR],
1743                    ),
1744                    secondary: SecondaryDeviceAttributes {
1745                        device_type: DeviceType::VT220,
1746                        firmware_version: 1,
1747                        rom_cartridge: 0,
1748                    },
1749                    tertiary: TertiaryDeviceAttributes { unit_id: 0 },
1750                })
1751            })
1752            .expect("callback should register");
1753
1754        terminal
1755    }
1756
1757    /// Move a value into distinct heap storage with an explicit byte-for-byte
1758    /// relocation so the test does not rely on optimizer or allocator behavior.
1759    fn relocate_into_new_box<T>(value: T) -> (Box<T>, usize, usize) {
1760        // Keep the source allocation alive without running T's destructor.
1761        // We need the bytes to remain initialized until after the copy.
1762        let src = Box::new(ManuallyDrop::new(value));
1763        let src_addr = std::ptr::from_ref(&**src).cast::<T>() as usize;
1764
1765        unsafe {
1766            let dst_layout = std::alloc::Layout::new::<T>();
1767            let dst_ptr = std::alloc::alloc(dst_layout).cast::<T>();
1768            if dst_ptr.is_null() {
1769                std::alloc::handle_alloc_error(dst_layout);
1770            }
1771
1772            let dst_addr = dst_ptr as usize;
1773            assert_ne!(
1774                src_addr, dst_addr,
1775                "test setup failed: source and destination storage unexpectedly match"
1776            );
1777
1778            // SAFETY: src points to a fully initialized T wrapped in
1779            // ManuallyDrop, dst points to distinct uninitialized storage for
1780            // exactly one T, and the regions do not overlap.
1781            std::ptr::copy_nonoverlapping(std::ptr::from_ref(&**src).cast::<T>(), dst_ptr, 1);
1782
1783            // SAFETY: src was allocated as Box<ManuallyDrop<T>> and must be
1784            // freed without dropping T because ownership was transferred by
1785            // the raw byte copy above.
1786            std::alloc::dealloc(
1787                Box::into_raw(src).cast::<u8>(),
1788                std::alloc::Layout::new::<ManuallyDrop<T>>(),
1789            );
1790
1791            // SAFETY: We just initialized dst_ptr by copying a valid T into it,
1792            // so it now owns exactly one initialized T allocation.
1793            (Box::from_raw(dst_ptr), src_addr, dst_addr)
1794        }
1795    }
1796
1797    /// Send an OSC 2 title sequence, then verify `term.title()` returns the
1798    /// correct value inside the `on_title_changed` callback.
1799    #[test]
1800    fn title_changed_callback_returns_correct_title() {
1801        // The callback bound on `on_title_changed` is `'cb`, not `'static`,
1802        // so the closure can borrow stack locals directly – no Rc needed.
1803        let captured_title: RefCell<String> = RefCell::new(String::new());
1804        let callback_count: Cell<usize> = Cell::new(0);
1805
1806        let mut terminal = Terminal::new(Options {
1807            cols: 80,
1808            rows: 24,
1809            max_scrollback: 0,
1810        })
1811        .expect("terminal should initialize");
1812
1813        terminal
1814            .on_title_changed(|term| {
1815                callback_count.set(callback_count.get() + 1);
1816                let title = term
1817                    .title()
1818                    .expect("title() should succeed inside callback");
1819                *captured_title.borrow_mut() = title.to_owned();
1820            })
1821            .expect("callback should register");
1822
1823        // OSC 2 (set title) should invoke on_title_changed.
1824        terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
1825        assert_eq!(callback_count.get(), 1);
1826        assert_eq!(*captured_title.borrow(), "Hello Effects");
1827
1828        // A second title change should fire the callback again.
1829        terminal.vt_write(b"\x1b]2;Second Title\x1b\\");
1830        assert_eq!(callback_count.get(), 2);
1831        assert_eq!(*captured_title.borrow(), "Second Title");
1832    }
1833
1834    /// Send an OSC 7 current-directory sequence, then verify `term.pwd()`
1835    /// returns the correct value inside the `on_pwd_changed` callback.
1836    #[test]
1837    fn pwd_changed_callback_returns_correct_pwd() {
1838        let captured_pwd: RefCell<String> = RefCell::new(String::new());
1839        let callback_count: Cell<usize> = Cell::new(0);
1840
1841        let mut terminal = Terminal::new(Options {
1842            cols: 80,
1843            rows: 24,
1844            max_scrollback: 0,
1845        })
1846        .expect("terminal should initialize");
1847
1848        terminal
1849            .on_pwd_changed(|term| {
1850                callback_count.set(callback_count.get() + 1);
1851                let pwd = term.pwd().expect("pwd() should succeed inside callback");
1852                *captured_pwd.borrow_mut() = pwd.to_owned();
1853            })
1854            .expect("callback should register");
1855
1856        terminal.vt_write(b"\x1b]7;file://localhost/tmp/project\x1b\\");
1857        assert_eq!(callback_count.get(), 1);
1858        assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/project");
1859
1860        terminal.vt_write(b"\x1b]7;file://localhost/tmp/other\x1b\\");
1861        assert_eq!(callback_count.get(), 2);
1862        assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/other");
1863    }
1864
1865    #[test]
1866    fn default_cursor_reset_uses_configured_style_and_blink() {
1867        let mut terminal = Terminal::new(Options {
1868            cols: 80,
1869            rows: 24,
1870            max_scrollback: 0,
1871        })
1872        .expect("terminal should initialize");
1873        let mut render_state = RenderState::new().expect("render state should initialize");
1874
1875        terminal
1876            .set_default_cursor_style(Some(CursorStyle::Underline))
1877            .expect("default cursor style should update")
1878            .set_default_cursor_blink(Some(true))
1879            .expect("default cursor blink should update");
1880
1881        terminal.vt_write(b"\x1b[0 q");
1882        let snapshot = render_state
1883            .update(&terminal)
1884            .expect("render state should update");
1885
1886        assert_eq!(
1887            snapshot
1888                .cursor_visual_style()
1889                .expect("cursor style should be readable"),
1890            CursorVisualStyle::Underline
1891        );
1892        assert!(
1893            snapshot
1894                .cursor_blinking()
1895                .expect("cursor blink should be readable")
1896        );
1897    }
1898
1899    #[test]
1900    fn glyph_protocol_enabled_setting_updates() {
1901        let mut terminal = Terminal::new(Options {
1902            cols: 80,
1903            rows: 24,
1904            max_scrollback: 0,
1905        })
1906        .expect("terminal should initialize");
1907
1908        terminal
1909            .set_glyph_protocol_enabled(false)
1910            .expect("glyph protocol should disable")
1911            .set_glyph_protocol_enabled(true)
1912            .expect("glyph protocol should enable");
1913    }
1914
1915    /// Explicitly relocate the Terminal into distinct storage, then verify the
1916    /// callback still fires through the stable VTable userdata pointer.
1917    #[test]
1918    fn callbacks_survive_explicit_relocation() {
1919        let callback_count = RefCell::new(0usize);
1920        let terminal = build_terminal(&callback_count);
1921        let (mut terminal, addr_before, addr_after) = relocate_into_new_box(terminal);
1922        assert_ne!(addr_before, addr_after);
1923
1924        // Primary DA request (CSI c) should invoke on_device_attributes.
1925        terminal.vt_write(b"\x1b[c");
1926        assert_eq!(*callback_count.borrow(), 1);
1927    }
1928
1929    fn tiny_terminal() -> Terminal<'static, 'static> {
1930        Terminal::new(Options {
1931            cols: 8,
1932            rows: 3,
1933            max_scrollback: 100,
1934        })
1935        .expect("terminal should initialize")
1936    }
1937
1938    fn codepoint_at_tracked_ref(terminal: &Terminal<'_, '_>, tracked: &TrackedGridRef) -> u32 {
1939        let snapshot = tracked
1940            .snapshot(terminal)
1941            .expect("tracked snapshot should not fail")
1942            .expect("tracked ref should have a value");
1943        snapshot
1944            .cell()
1945            .expect("tracked snapshot should resolve to a cell")
1946            .codepoint()
1947            .expect("tracked snapshot cell should expose a codepoint")
1948    }
1949
1950    #[test]
1951    fn tracked_grid_ref_follows_scroll() {
1952        let mut terminal = tiny_terminal();
1953        terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1954
1955        let tracked = terminal
1956            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1957            .expect("tracked grid ref should initialize");
1958
1959        terminal.vt_write(b"\r\ndelta");
1960
1961        assert!(tracked.has_value());
1962        assert_eq!(
1963            codepoint_at_tracked_ref(&terminal, &tracked),
1964            u32::from('a')
1965        );
1966        assert_eq!(
1967            tracked
1968                .point(PointSpace::Screen)
1969                .expect("tracked point should resolve")
1970                .expect("tracked point should have a value")
1971                .x,
1972            0
1973        );
1974    }
1975
1976    #[test]
1977    fn tracked_grid_ref_reports_loss_and_can_set_point() {
1978        let mut terminal = tiny_terminal();
1979        terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1980
1981        let mut tracked = terminal
1982            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1983            .expect("tracked grid ref should initialize");
1984
1985        terminal.reset();
1986
1987        assert!(!tracked.has_value());
1988        assert!(
1989            tracked
1990                .snapshot(&terminal)
1991                .expect("missing tracked snapshot should not fail")
1992                .is_none()
1993        );
1994        assert!(
1995            tracked
1996                .point(PointSpace::Screen)
1997                .expect("missing tracked point should not fail")
1998                .is_none()
1999        );
2000
2001        terminal.vt_write(b"echo");
2002        tracked
2003            .set(&mut terminal, Point::Active(PointCoordinate { x: 0, y: 0 }))
2004            .expect("tracked grid ref should set to a new point");
2005
2006        assert!(tracked.has_value());
2007        assert_eq!(
2008            codepoint_at_tracked_ref(&terminal, &tracked),
2009            u32::from('e')
2010        );
2011    }
2012
2013    #[test]
2014    fn tracked_grid_ref_survives_terminal_drop() {
2015        let tracked = {
2016            let mut terminal = tiny_terminal();
2017            terminal.vt_write(b"alpha");
2018            terminal
2019                .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
2020                .expect("tracked grid ref should initialize")
2021        };
2022
2023        assert!(!tracked.has_value());
2024        assert!(
2025            tracked
2026                .point(PointSpace::Screen)
2027                .expect("detached tracked point should not fail")
2028                .is_none()
2029        );
2030    }
2031
2032    #[test]
2033    fn tracked_grid_ref_rejects_different_terminal() {
2034        let mut first = tiny_terminal();
2035        first.vt_write(b"alpha");
2036        let mut second = tiny_terminal();
2037        second.vt_write(b"bravo");
2038
2039        let mut tracked = first
2040            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
2041            .expect("tracked grid ref should initialize");
2042
2043        assert!(matches!(
2044            tracked.snapshot(&second),
2045            Err(Error::InvalidValue)
2046        ));
2047        assert!(matches!(
2048            tracked.set(&mut second, Point::Active(PointCoordinate { x: 0, y: 0 })),
2049            Err(Error::InvalidValue)
2050        ));
2051    }
2052
2053    #[test]
2054    fn grid_ref_converts_back_to_point() {
2055        let mut terminal = tiny_terminal();
2056        terminal.vt_write(b"alpha");
2057
2058        let original = PointCoordinate { x: 1, y: 0 };
2059        let grid_ref = terminal
2060            .grid_ref(Point::Active(original))
2061            .expect("grid ref should resolve");
2062
2063        assert_eq!(
2064            terminal
2065                .point_from_grid_ref(&grid_ref, PointSpace::Active)
2066                .expect("grid ref point conversion should not fail")
2067                .expect("grid ref should be representable in active space"),
2068            original
2069        );
2070    }
2071}
2072
2073/// Soundness regression tests for
2074/// <https://github.com/Uzaaft/libghostty-rs/issues/74>.
2075///
2076/// These tests are gated on `cfg(miri)` because they construct the exact
2077/// shapes the C API produces and feed them into the safe wrappers, which was
2078/// UB before the wrappers stopped building slices and `&str` from unvalidated
2079/// FFI input. Run with:
2080///
2081/// ```sh
2082/// cargo +nightly miri test -p libghostty-vt miri_soundness
2083/// ```
2084#[cfg(all(test, miri))]
2085mod miri_soundness {
2086    use super::*;
2087
2088    /// The C trampoline declares `contents: ?[*]const ClipboardContent` and
2089    /// sends `contents = NULL, contents_len = 0` for a write carrying no
2090    /// representations (e.g. OSC 52 with an empty payload, the documented
2091    /// "clear the clipboard" shape). `slice::from_raw_parts` requires a
2092    /// non-null pointer even at length zero, so `contents()` used to be UB
2093    /// here; it must yield an empty iterator so hosts can observe "clear".
2094    #[test]
2095    fn clipboard_write_with_no_representations() {
2096        let raw = ffi::ClipboardWrite {
2097            size: std::mem::size_of::<ffi::ClipboardWrite>(),
2098            location: ffi::ClipboardLocation::STANDARD,
2099            contents: std::ptr::null(),
2100            contents_len: 0,
2101        };
2102        // SAFETY: `raw` outlives the borrow, matching the callback contract.
2103        let write = unsafe { ClipboardWrite::from_raw(&raw) };
2104        assert_eq!(write.contents().count(), 0);
2105    }
2106
2107    /// OSC 52 payloads are base64-decoded arbitrary bytes ("binary-safe" per
2108    /// the C header), but `ClipboardContent` used to expose them as `&str`
2109    /// built with `str::from_utf8_unchecked` in the sys crate, so decoding
2110    /// the invalid `&str` entered unreachable code in std's UTF-8 decoder.
2111    /// Within 0.2.x the field stays `&str`, so invalid UTF-8 must degrade
2112    /// to an empty string rather than an invalid `&str`.
2113    #[test]
2114    fn clipboard_content_with_non_utf8_data() {
2115        // OSC 52 payload "//4=" base64-decodes to FF FE, which is not UTF-8.
2116        let data = [0xFF_u8, 0xFE];
2117        let raw = ffi::ClipboardContent {
2118            mime: ffi::String::from("text/plain"),
2119            data: ffi::String {
2120                ptr: data.as_ptr(),
2121                len: data.len(),
2122            },
2123        };
2124        // SAFETY: `data` outlives the borrow, matching the callback contract.
2125        let content = unsafe { ClipboardContent::from_raw(&raw) };
2126        assert_eq!(content.mime, "text/plain");
2127        assert_eq!(content.data, "");
2128        // Decoding the invalid `&str` is what Miri used to flag; keep
2129        // exercising it. `next()` is deliberate: `Chars::count()` is
2130        // specialized to count byte patterns and never decodes.
2131        assert_eq!(content.data.chars().next(), None);
2132    }
2133
2134    /// `mime` stays `&str`, so it must be validated rather than trusted:
2135    /// a non-UTF-8 mime string falls back to the opaque-bytes mime type
2136    /// instead of producing an invalid `&str`.
2137    #[test]
2138    fn clipboard_content_with_non_utf8_mime() {
2139        let mime = [0xFF_u8, 0xFE];
2140        let data = *b"hello";
2141        let raw = ffi::ClipboardContent {
2142            mime: ffi::String {
2143                ptr: mime.as_ptr(),
2144                len: mime.len(),
2145            },
2146            data: ffi::String {
2147                ptr: data.as_ptr(),
2148                len: data.len(),
2149            },
2150        };
2151        // SAFETY: `mime` and `data` outlive the borrow, matching the
2152        // callback contract.
2153        let content = unsafe { ClipboardContent::from_raw(&raw) };
2154        assert_eq!(content.mime, "application/octet-stream");
2155        assert_eq!(content.data, "hello");
2156    }
2157}