Skip to main content

libghostty_vt/
terminal.rs

1//! Types and functions around terminal state management.
2
3use std::{mem::MaybeUninit, ptr::NonNull};
4
5use crate::{
6    alloc::{Allocator, Object},
7    error::{Error, Result, from_optional_result_uninit, from_result},
8    ffi::{self, TerminalData as Data, TerminalOption as Opt},
9    key,
10    screen::{GridRef, Screen, TrackedGridRef},
11    style::{self, 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[PaletteIndex::BLACK.0 as usize]   = RgbColor { r: 0x45, g: 0x47, b: 0x5A };
216///     palette[PaletteIndex::RED.0 as usize]     = RgbColor { r: 0xF3, g: 0x8B, b: 0xA8 };
217///     palette[PaletteIndex::GREEN.0 as usize]   = RgbColor { r: 0xA6, g: 0xE3, b: 0xA1 };
218///     palette[PaletteIndex::YELLOW.0 as usize]  = RgbColor { r: 0xF9, g: 0xE2, b: 0xAF };
219///     palette[PaletteIndex::BLUE.0 as usize]    = RgbColor { r: 0x89, g: 0xB4, b: 0xFA };
220///     palette[PaletteIndex::MAGENTA.0 as usize] = RgbColor { r: 0xF5, g: 0xC2, b: 0xE7 };
221///     palette[PaletteIndex::CYAN.0 as usize]    = RgbColor { r: 0x94, g: 0xE2, b: 0xD5 };
222///     palette[PaletteIndex::WHITE.0 as usize]   = 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    pub(crate) fn get<T>(&self, tag: ffi::TerminalData::Type) -> Result<T> {
465        let mut value = MaybeUninit::<T>::zeroed();
466        let result = unsafe {
467            ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
468        };
469        from_result(result)?;
470        // SAFETY: Value should be initialized after successful call.
471        Ok(unsafe { value.assume_init() })
472    }
473    pub(crate) fn get_optional<T>(&self, tag: ffi::TerminalData::Type) -> Result<Option<T>> {
474        let mut value = MaybeUninit::<T>::zeroed();
475        let result = unsafe {
476            ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
477        };
478        from_optional_result_uninit(result, value)
479    }
480    pub(crate) fn set<T>(&self, tag: ffi::TerminalOption::Type, v: &T) -> Result<()> {
481        let result = unsafe {
482            ffi::ghostty_terminal_set(self.inner.as_raw(), tag, std::ptr::from_ref(v).cast())
483        };
484        from_result(result)
485    }
486    /// Set an option whose ABI expects the pointer value itself, not a pointer
487    /// to Rust storage containing that value.
488    pub(crate) fn set_ptr(
489        &self,
490        tag: ffi::TerminalOption::Type,
491        ptr: *const std::ffi::c_void,
492    ) -> Result<()> {
493        let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr) };
494        from_result(result)
495    }
496    pub(crate) fn set_optional<T>(
497        &self,
498        tag: ffi::TerminalOption::Type,
499        v: Option<&T>,
500    ) -> Result<()> {
501        let ptr = if let Some(v) = v {
502            std::ptr::from_ref(v)
503        } else {
504            std::ptr::null()
505        };
506
507        let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr.cast()) };
508        from_result(result)
509    }
510
511    /// Get the terminal width in cells.
512    pub fn cols(&self) -> Result<u16> {
513        self.get(Data::COLS)
514    }
515    /// Get the terminal height in cells.
516    pub fn rows(&self) -> Result<u16> {
517        self.get(Data::ROWS)
518    }
519    /// Get the cursor column position (inner-indexed).
520    pub fn cursor_x(&self) -> Result<u16> {
521        self.get(Data::CURSOR_X)
522    }
523    /// Get the cursor row position within the active area (inner-indexed).
524    pub fn cursor_y(&self) -> Result<u16> {
525        self.get(Data::CURSOR_Y)
526    }
527    /// Get whether the cursor has a pending wrap (next print will soft-wrap).
528    pub fn is_cursor_pending_wrap(&self) -> Result<bool> {
529        self.get(Data::CURSOR_PENDING_WRAP)
530    }
531    /// Get whether the cursor is visible (DEC mode 25).
532    pub fn is_cursor_visible(&self) -> Result<bool> {
533        self.get(Data::CURSOR_VISIBLE)
534    }
535    /// Get the current SGR style of the cursor.
536    ///
537    /// This is the style that will be applied to newly printed characters.
538    pub fn cursor_style(&self) -> Result<style::Style> {
539        self.get::<ffi::Style>(Data::CURSOR_STYLE)
540            .and_then(std::convert::TryInto::try_into)
541    }
542    /// Get the current Kitty keyboard protocol flags.
543    pub fn kitty_keyboard_flags(&self) -> Result<key::KittyKeyFlags> {
544        self.get::<ffi::KittyKeyFlags>(Data::KITTY_KEYBOARD_FLAGS)
545            .map(key::KittyKeyFlags::from_bits_retain)
546    }
547
548    /// Get the scrollbar state for the terminal viewport.
549    ///
550    /// This may be expensive to calculate depending on where the viewport is
551    /// (arbitrary pins are expensive). The caller should take care to only call
552    /// this as needed and not too frequently.
553    pub fn scrollbar(&self) -> Result<Scrollbar> {
554        self.get(Data::SCROLLBAR)
555    }
556    /// Get the currently active screen.
557    pub fn active_screen(&self) -> Result<Screen> {
558        self.get(Data::ACTIVE_SCREEN)
559    }
560    /// Get whether any mouse tracking mode is active.
561    ///
562    /// Returns true if any of the mouse tracking modes (X1inner, normal, button,
563    /// or any-event) are enabled.
564    pub fn is_mouse_tracking(&self) -> Result<bool> {
565        self.get(Data::MOUSE_TRACKING)
566    }
567    /// Get the terminal title as set by escape sequences (e.g. OSC inner/2).
568    ///
569    /// Returns a borrowed string, valid until the next call to
570    /// [`Terminal::vt_write`] or [`Terminal::reset`]. An empty string is
571    /// returned when no title has been set.
572    pub fn title(&self) -> Result<&str> {
573        let str = self.get::<ffi::String>(Data::TITLE)?;
574        // SAFETY: We trust libghostty to return a valid borrowed string,
575        // while we uphold that no mutation could happen during its lifetime.
576        let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
577        std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
578    }
579
580    /// Get the current working directory as set by escape sequences (e.g. OSC 7).
581    ///
582    /// Returns a borrowed string, valid until the next call to
583    /// [`Terminal::vt_write`] or [`Terminal::reset`]. An empty string is
584    /// returned when no title has been set.
585    pub fn pwd(&self) -> Result<&str> {
586        let str = self.get::<ffi::String>(Data::PWD)?;
587        // SAFETY: We trust libghostty to return a valid borrowed string,
588        // while we uphold that no mutation could happen during its lifetime.
589        let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
590        std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
591    }
592    /// The total number of rows in the active screen including scrollback.
593    pub fn total_rows(&self) -> Result<usize> {
594        self.get(Data::TOTAL_ROWS)
595    }
596    ///  The number of scrollback rows (total rows minus viewport rows).
597    pub fn scrollback_rows(&self) -> Result<usize> {
598        self.get(Data::SCROLLBACK_ROWS)
599    }
600
601    /// The effective foreground color (override or default).
602    pub fn fg_color(&self) -> Result<Option<RgbColor>> {
603        self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND)
604            .map(|v| v.map(Into::into))
605    }
606    /// The default foreground color (ignoring any OSC override).
607    pub fn default_fg_color(&self) -> Result<Option<RgbColor>> {
608        self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND_DEFAULT)
609            .map(|v| v.map(Into::into))
610    }
611    /// Set the default foreground color.
612    pub fn set_default_fg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
613        self.set_optional(Opt::COLOR_FOREGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
614        Ok(self)
615    }
616
617    /// The effective background color (override or default).
618    pub fn bg_color(&self) -> Result<Option<RgbColor>> {
619        self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND)
620            .map(|v| v.map(Into::into))
621    }
622    /// The default background color (ignoring any OSC override).
623    pub fn default_bg_color(&self) -> Result<Option<RgbColor>> {
624        self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND_DEFAULT)
625            .map(|v| v.map(Into::into))
626    }
627    /// Set the default background color.
628    pub fn set_default_bg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
629        self.set_optional(Opt::COLOR_BACKGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
630        Ok(self)
631    }
632
633    /// The effective cursor color (override or default).
634    pub fn cursor_color(&self) -> Result<Option<RgbColor>> {
635        self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR)
636            .map(|v| v.map(Into::into))
637    }
638    /// The default cursor color (ignoring any OSC override).
639    pub fn default_cursor_color(&self) -> Result<Option<RgbColor>> {
640        self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR_DEFAULT)
641            .map(|v| v.map(Into::into))
642    }
643    /// Set the default cursor color.
644    pub fn set_default_cursor_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
645        self.set_optional(Opt::COLOR_CURSOR, v.map(ffi::ColorRgb::from).as_ref())?;
646        Ok(self)
647    }
648
649    /// Set the default cursor style used by DECSCUSR reset (CSI 0 q).
650    ///
651    /// Passing `None` resets to libghostty's built-in block cursor default.
652    pub fn set_default_cursor_style(&mut self, v: Option<CursorStyle>) -> Result<&mut Self> {
653        self.set_optional(Opt::DEFAULT_CURSOR_STYLE, v.as_ref())?;
654        Ok(self)
655    }
656
657    /// Set whether the default cursor blinks when reset by DECSCUSR (CSI 0 q).
658    ///
659    /// Passing `None` resets to libghostty's built-in non-blinking default.
660    pub fn set_default_cursor_blink(&mut self, v: Option<bool>) -> Result<&mut Self> {
661        self.set_optional(Opt::DEFAULT_CURSOR_BLINK, v.as_ref())?;
662        Ok(self)
663    }
664
665    /// The current 256-color palette.
666    pub fn color_palette(&self) -> Result<[RgbColor; 256]> {
667        self.get::<[ffi::ColorRgb; 256]>(Data::COLOR_PALETTE)
668            .map(|v| v.map(Into::into))
669    }
670    /// The default 256-color palette (ignoring any OSC overrides).
671    pub fn default_color_palette(&self) -> Result<[RgbColor; 256]> {
672        self.get::<[ffi::ColorRgb; 256]>(Data::COLOR_PALETTE_DEFAULT)
673            .map(|v| v.map(Into::into))
674    }
675    /// Set the default 256-color palette.
676    pub fn set_default_color_palette(&mut self, v: Option<[RgbColor; 256]>) -> Result<&mut Self> {
677        self.set_optional(
678            Opt::COLOR_PALETTE,
679            v.map(|v| v.map(ffi::ColorRgb::from)).as_ref(),
680        )?;
681        Ok(self)
682    }
683
684    /// Set the maximum bytes the APC handler will buffer for all protocols.
685    ///
686    /// This prevents malicious input from causing unbounded memory allocation.
687    /// A `None` value removes all overrides, reverting to the built-in defaults.
688    pub fn set_apc_max_bytes(&mut self, max: Option<usize>) -> Result<&mut Self> {
689        self.set_optional(ffi::TerminalOption::APC_MAX_BYTES, max.as_ref())?;
690        Ok(self)
691    }
692
693    /// Enable or disable Glyph Protocol APC handling.
694    ///
695    /// Disabling the protocol makes the terminal ignore Glyph Protocol APC
696    /// sequences and clears the session's glyph glossary.
697    pub fn set_glyph_protocol_enabled(&mut self, enabled: bool) -> Result<&mut Self> {
698        self.set(ffi::TerminalOption::GLYPH_PROTOCOL, &enabled)?;
699        Ok(self)
700    }
701}
702impl Drop for Terminal<'_, '_> {
703    fn drop(&mut self) {
704        unsafe { ffi::ghostty_terminal_free(self.inner.as_raw()) }
705    }
706}
707
708/// A point in the terminal grid.
709#[derive(Clone, Copy, Debug, PartialEq, Eq)]
710pub enum Point {
711    /// Active area where the cursor can move.
712    Active(PointCoordinate),
713    /// Visible viewport (changes when scrolled).
714    Viewport(PointCoordinate),
715    /// Full screen including scrollback.
716    Screen(PointCoordinate),
717    /// Scrollback history only (before active area).
718    History(PointCoordinate),
719}
720
721impl From<Point> for ffi::Point {
722    fn from(value: Point) -> Self {
723        match value {
724            Point::Active(coord) => Self {
725                tag: ffi::PointTag::ACTIVE,
726                value: ffi::PointValue {
727                    coordinate: coord.into(),
728                },
729            },
730            Point::Viewport(coord) => Self {
731                tag: ffi::PointTag::VIEWPORT,
732                value: ffi::PointValue {
733                    coordinate: coord.into(),
734                },
735            },
736            Point::Screen(coord) => Self {
737                tag: ffi::PointTag::SCREEN,
738                value: ffi::PointValue {
739                    coordinate: coord.into(),
740                },
741            },
742            Point::History(coord) => Self {
743                tag: ffi::PointTag::HISTORY,
744                value: ffi::PointValue {
745                    coordinate: coord.into(),
746                },
747            },
748        }
749    }
750}
751
752/// A coordinate space for converting grid references back to points.
753#[derive(Clone, Copy, Debug, PartialEq, Eq)]
754pub enum PointSpace {
755    /// Active area where the cursor can move.
756    Active,
757    /// Visible viewport, which changes when scrolled.
758    Viewport,
759    /// Full screen including scrollback.
760    Screen,
761    /// Scrollback history only, before the active area.
762    History,
763}
764
765impl PointSpace {
766    pub(crate) fn into_raw(self) -> ffi::PointTag::Type {
767        match self {
768            Self::Active => ffi::PointTag::ACTIVE,
769            Self::Viewport => ffi::PointTag::VIEWPORT,
770            Self::Screen => ffi::PointTag::SCREEN,
771            Self::History => ffi::PointTag::HISTORY,
772        }
773    }
774}
775
776/// A coordinate in the terminal grid.
777#[derive(Clone, Copy, Debug, PartialEq, Eq)]
778pub struct PointCoordinate {
779    /// Column (0-indexed).
780    pub x: u16,
781    /// Row (0-indexed). May exceed page size for screen/history tags.
782    pub y: u32,
783}
784impl From<PointCoordinate> for ffi::PointCoordinate {
785    fn from(value: PointCoordinate) -> Self {
786        let PointCoordinate { x, y } = value;
787        Self { x, y }
788    }
789}
790impl From<ffi::PointCoordinate> for PointCoordinate {
791    fn from(value: ffi::PointCoordinate) -> Self {
792        let ffi::PointCoordinate { x, y } = value;
793        Self { x, y }
794    }
795}
796
797/// Scroll viewport behavior.
798#[derive(Clone, Copy, Debug, PartialEq, Eq)]
799pub enum ScrollViewport {
800    /// Scroll to the top of the scrollback.
801    Top,
802    /// Scroll to the bottom (active area).
803    Bottom,
804    /// Scroll by a delta amount (up is negative).
805    Delta(isize),
806}
807impl From<ScrollViewport> for ffi::TerminalScrollViewport {
808    fn from(value: ScrollViewport) -> Self {
809        match value {
810            ScrollViewport::Top => Self {
811                tag: ffi::TerminalScrollViewportTag::TOP,
812                value: ffi::TerminalScrollViewportValue::default(),
813            },
814            ScrollViewport::Bottom => Self {
815                tag: ffi::TerminalScrollViewportTag::BOTTOM,
816                value: ffi::TerminalScrollViewportValue::default(),
817            },
818            ScrollViewport::Delta(delta) => Self {
819                tag: ffi::TerminalScrollViewportTag::DELTA,
820                value: {
821                    let mut v = ffi::TerminalScrollViewportValue::default();
822                    v.delta = delta;
823                    v
824                },
825            },
826        }
827    }
828}
829
830/// A terminal mode consisting of its value and its kind (DEC/ANSI).
831#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
832pub struct Mode(pub ffi::Mode);
833
834impl Mode {
835    #![expect(missing_docs, reason = "no upstream documentation provided")]
836    const ANSI_BIT: u16 = 1 << 15;
837
838    /// Create a new mode from its numeric value and its kind.
839    #[must_use]
840    pub const fn new(v: u16, kind: ModeKind) -> Self {
841        match kind {
842            ModeKind::Ansi => Self(v | Self::ANSI_BIT),
843            ModeKind::Dec => Self(v),
844        }
845    }
846
847    /// The numeric value of the mode.
848    #[must_use]
849    pub const fn value(self) -> u16 {
850        (self.0) & 0x7fff
851    }
852
853    /// The kind of the mode (DEC/ANSI).
854    #[must_use]
855    pub const fn kind(self) -> ModeKind {
856        if (self.0) & Self::ANSI_BIT > 0 {
857            ModeKind::Ansi
858        } else {
859            ModeKind::Dec
860        }
861    }
862
863    pub const KAM: Self = Self::new(2, ModeKind::Ansi);
864    pub const INSERT: Self = Self::new(4, ModeKind::Ansi);
865    pub const SRM: Self = Self::new(12, ModeKind::Ansi);
866    pub const LINEFEED: Self = Self::new(20, ModeKind::Ansi);
867
868    pub const DECCKM: Self = Self::new(1, ModeKind::Dec);
869    pub const _132_COLUMN: Self = Self::new(3, ModeKind::Dec);
870    pub const SLOW_SCROLL: Self = Self::new(4, ModeKind::Dec);
871    pub const REVERSE_COLORS: Self = Self::new(5, ModeKind::Dec);
872    pub const ORIGIN: Self = Self::new(6, ModeKind::Dec);
873    pub const WRAPAROUND: Self = Self::new(7, ModeKind::Dec);
874    pub const AUTOREPEAT: Self = Self::new(8, ModeKind::Dec);
875    pub const X10_MOUSE: Self = Self::new(9, ModeKind::Dec);
876    pub const CURSOR_BLINKING: Self = Self::new(12, ModeKind::Dec);
877    pub const CURSOR_VISIBLE: Self = Self::new(25, ModeKind::Dec);
878    pub const ENABLE_MODE3: Self = Self::new(40, ModeKind::Dec);
879    pub const REVERSE_WRAP: Self = Self::new(45, ModeKind::Dec);
880    pub const ALT_SCREEN_LEGACY: Self = Self::new(47, ModeKind::Dec);
881    pub const KEYPAD_KEYS: Self = Self::new(66, ModeKind::Dec);
882    pub const LEFT_RIGHT_MARGIN: Self = Self::new(69, ModeKind::Dec);
883    pub const NORMAL_MOUSE: Self = Self::new(1000, ModeKind::Dec);
884    pub const BUTTON_MOUSE: Self = Self::new(1002, ModeKind::Dec);
885    pub const ANY_MOUSE: Self = Self::new(1003, ModeKind::Dec);
886    pub const FOCUS_EVENT: Self = Self::new(1004, ModeKind::Dec);
887    pub const UTF8_MOUSE: Self = Self::new(1005, ModeKind::Dec);
888    pub const SGR_MOUSE: Self = Self::new(1006, ModeKind::Dec);
889    pub const ALT_SCROLL: Self = Self::new(1007, ModeKind::Dec);
890    pub const URXVT_MOUSE: Self = Self::new(1015, ModeKind::Dec);
891    pub const SGR_PIXELS_MOUSE: Self = Self::new(1016, ModeKind::Dec);
892    pub const NUMLOCK_KEYPAD: Self = Self::new(1035, ModeKind::Dec);
893    pub const ALT_ESC_PREFIX: Self = Self::new(1036, ModeKind::Dec);
894    pub const ALT_SENDS_ESC: Self = Self::new(1039, ModeKind::Dec);
895    pub const REVERSE_WRAP_EXT: Self = Self::new(1045, ModeKind::Dec);
896    pub const ALT_SCREEN: Self = Self::new(1047, ModeKind::Dec);
897    pub const SAVE_CURSOR: Self = Self::new(1048, ModeKind::Dec);
898    pub const ALT_SCREEN_SAVE: Self = Self::new(1049, ModeKind::Dec);
899    pub const BRACKETED_PASTE: Self = Self::new(2004, ModeKind::Dec);
900    pub const SYNC_OUTPUT: Self = Self::new(2026, ModeKind::Dec);
901    pub const GRAPHEME_CLUSTER: Self = Self::new(2027, ModeKind::Dec);
902    pub const COLOR_SCHEME_REPORT: Self = Self::new(2031, ModeKind::Dec);
903    pub const IN_BAND_RESIZE: Self = Self::new(2048, ModeKind::Dec);
904}
905
906/// The kind of a terminal mode.
907#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
908pub enum ModeKind {
909    /// DEC terminal mode.
910    Dec,
911    /// ANSI terminal mode.
912    Ansi,
913}
914
915impl From<Mode> for ffi::Mode {
916    fn from(value: Mode) -> Self {
917        value.0
918    }
919}
920
921/// Device attributes response data for all three DA levels.
922/// Filled by the [`Terminal::on_device_attributes`] callback in response
923/// to CSI c, CSI > c, or CSI = c queries. The terminal uses whichever
924/// sub-struct matches the request type.
925#[derive(Debug, Clone, Copy)]
926pub struct DeviceAttributes {
927    /// Primary device attributes (DA1).
928    pub primary: PrimaryDeviceAttributes,
929    /// Secondary device attributes (DA2).
930    pub secondary: SecondaryDeviceAttributes,
931    /// Tertiary device attributes (DA3).
932    pub tertiary: TertiaryDeviceAttributes,
933}
934
935impl From<DeviceAttributes> for ffi::DeviceAttributes {
936    fn from(value: DeviceAttributes) -> Self {
937        Self {
938            primary: value.primary.into(),
939            secondary: value.secondary.into(),
940            tertiary: value.tertiary.into(),
941        }
942    }
943}
944
945/// Primary device attributes (DA1) response data.
946///
947/// Returned as part of [`DeviceAttributes`] in response to a CSI c query.
948#[derive(Debug, Clone, Copy)]
949pub struct PrimaryDeviceAttributes(ffi::DeviceAttributesPrimary);
950
951impl PrimaryDeviceAttributes {
952    /// Construct primary device attributes from a conformance level
953    /// and an array of device attribute features.
954    ///
955    /// Prefer defining primary device attributes as a `const` when the feature
956    /// list is statically known. That makes the 64-feature limit fail during
957    /// compilation instead of panicking at runtime.
958    ///
959    /// # Panics
960    ///
961    /// **Panics** when more than 64 features are given.
962    #[must_use]
963    pub const fn new(
964        conformance_level: ConformanceLevel,
965        features: &[DeviceAttributeFeature],
966    ) -> Self {
967        assert!(features.len() <= 64);
968
969        let mut f = [0u16; 64];
970        let mut i = 0;
971        while i < features.len() {
972            f[i] = features[i].0;
973            i += 1;
974        }
975
976        Self(ffi::DeviceAttributesPrimary {
977            conformance_level: conformance_level.0,
978            features: f,
979            num_features: features.len(),
980        })
981    }
982}
983
984impl From<PrimaryDeviceAttributes> for ffi::DeviceAttributesPrimary {
985    fn from(value: PrimaryDeviceAttributes) -> Self {
986        value.0
987    }
988}
989
990/// The level of conformance to the behavior of a specific or a family of
991/// physical terminal models.
992#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
993pub struct ConformanceLevel(pub u16);
994
995impl ConformanceLevel {
996    #![expect(clippy::doc_markdown, reason = "false positive")]
997    #![expect(missing_docs, reason = "self-explanatory")]
998    pub const VT100: Self = Self(ffi::DA_CONFORMANCE_VT100);
999    pub const VT101: Self = Self(ffi::DA_CONFORMANCE_VT101);
1000    pub const VT102: Self = Self(ffi::DA_CONFORMANCE_VT102);
1001    pub const VT125: Self = Self(ffi::DA_CONFORMANCE_VT125);
1002    pub const VT131: Self = Self(ffi::DA_CONFORMANCE_VT131);
1003    pub const VT132: Self = Self(ffi::DA_CONFORMANCE_VT132);
1004    pub const VT220: Self = Self(ffi::DA_CONFORMANCE_VT220);
1005    pub const VT240: Self = Self(ffi::DA_CONFORMANCE_VT240);
1006    pub const VT320: Self = Self(ffi::DA_CONFORMANCE_VT320);
1007    pub const VT340: Self = Self(ffi::DA_CONFORMANCE_VT340);
1008    pub const VT420: Self = Self(ffi::DA_CONFORMANCE_VT420);
1009    pub const VT510: Self = Self(ffi::DA_CONFORMANCE_VT510);
1010    pub const VT520: Self = Self(ffi::DA_CONFORMANCE_VT520);
1011    pub const VT525: Self = Self(ffi::DA_CONFORMANCE_VT525);
1012    /// Equivalent to a VT2xx terminal.
1013    pub const LEVEL_2: Self = Self(ffi::DA_CONFORMANCE_LEVEL_2);
1014    /// Equivalent to a VT3xx terminal.
1015    pub const LEVEL_3: Self = Self(ffi::DA_CONFORMANCE_LEVEL_3);
1016    /// Equivalent to a VT4xx terminal.
1017    pub const LEVEL_4: Self = Self(ffi::DA_CONFORMANCE_LEVEL_4);
1018    /// Equivalent to a VT5xx terminal.
1019    pub const LEVEL_5: Self = Self(ffi::DA_CONFORMANCE_LEVEL_5);
1020}
1021
1022/// A feature that a terminal can report to support.
1023#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1024pub struct DeviceAttributeFeature(pub u16);
1025
1026impl DeviceAttributeFeature {
1027    #![expect(missing_docs, reason = "no upstream documentation provided")]
1028    pub const COLUMNS_132: Self = Self(ffi::DA_FEATURE_COLUMNS_132);
1029    pub const PRINTER: Self = Self(ffi::DA_FEATURE_PRINTER);
1030    pub const REGIS: Self = Self(ffi::DA_FEATURE_REGIS);
1031    pub const SIXEL: Self = Self(ffi::DA_FEATURE_SIXEL);
1032    pub const SELECTIVE_ERASE: Self = Self(ffi::DA_FEATURE_SELECTIVE_ERASE);
1033    pub const USER_DEFINED_KEYS: Self = Self(ffi::DA_FEATURE_USER_DEFINED_KEYS);
1034    pub const NATIONAL_REPLACEMENT: Self = Self(ffi::DA_FEATURE_NATIONAL_REPLACEMENT);
1035    pub const TECHNICAL_CHARACTERS: Self = Self(ffi::DA_FEATURE_TECHNICAL_CHARACTERS);
1036    pub const LOCATOR: Self = Self(ffi::DA_FEATURE_LOCATOR);
1037    pub const TERMINAL_STATE: Self = Self(ffi::DA_FEATURE_TERMINAL_STATE);
1038    pub const WINDOWING: Self = Self(ffi::DA_FEATURE_WINDOWING);
1039    pub const HORIZONTAL_SCROLLING: Self = Self(ffi::DA_FEATURE_HORIZONTAL_SCROLLING);
1040    pub const ANSI_COLOR: Self = Self(ffi::DA_FEATURE_ANSI_COLOR);
1041    pub const RECTANGULAR_EDITING: Self = Self(ffi::DA_FEATURE_RECTANGULAR_EDITING);
1042    pub const ANSI_TEXT_LOCATOR: Self = Self(ffi::DA_FEATURE_ANSI_TEXT_LOCATOR);
1043    pub const CLIPBOARD: Self = Self(ffi::DA_FEATURE_CLIPBOARD);
1044}
1045
1046/// Secondary device attributes (DA2) response data.
1047///
1048/// Returned as part of [`DeviceAttributes`] in response to a CSI > c query.
1049/// Response format: CSI > Pp ; Pv ; Pc c
1050#[derive(Debug, Copy, Clone)]
1051pub struct SecondaryDeviceAttributes {
1052    /// Terminal type identifier (Pp).
1053    pub device_type: DeviceType,
1054    /// Firmware/patch version number (Pv).
1055    pub firmware_version: u16,
1056    /// ROM cartridge registration number (Pc). Always 0 for emulators.
1057    pub rom_cartridge: u16,
1058}
1059
1060impl From<SecondaryDeviceAttributes> for ffi::DeviceAttributesSecondary {
1061    fn from(value: SecondaryDeviceAttributes) -> Self {
1062        Self {
1063            device_type: value.device_type.0,
1064            firmware_version: value.firmware_version,
1065            rom_cartridge: value.rom_cartridge,
1066        }
1067    }
1068}
1069
1070/// The type of terminal device being emulated.
1071#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1072pub struct DeviceType(pub u16);
1073
1074impl DeviceType {
1075    #![expect(missing_docs, reason = "self-explanatory")]
1076    pub const VT100: Self = Self(ffi::DA_DEVICE_TYPE_VT100);
1077    pub const VT220: Self = Self(ffi::DA_DEVICE_TYPE_VT220);
1078    pub const VT240: Self = Self(ffi::DA_DEVICE_TYPE_VT240);
1079    pub const VT330: Self = Self(ffi::DA_DEVICE_TYPE_VT330);
1080    pub const VT340: Self = Self(ffi::DA_DEVICE_TYPE_VT340);
1081    pub const VT320: Self = Self(ffi::DA_DEVICE_TYPE_VT320);
1082    pub const VT382: Self = Self(ffi::DA_DEVICE_TYPE_VT382);
1083    pub const VT420: Self = Self(ffi::DA_DEVICE_TYPE_VT420);
1084    pub const VT510: Self = Self(ffi::DA_DEVICE_TYPE_VT510);
1085    pub const VT520: Self = Self(ffi::DA_DEVICE_TYPE_VT520);
1086    pub const VT525: Self = Self(ffi::DA_DEVICE_TYPE_VT525);
1087}
1088
1089/// Tertiary device attributes (DA3) response data.
1090///
1091/// Returned as part of [`DeviceAttributes`] in response to a CSI = c query.
1092/// Response format: DCS ! | D...D ST (DECRPTUI).
1093#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1094pub struct TertiaryDeviceAttributes {
1095    /// Unit ID encoded as 8 uppercase hex digits in the response.
1096    pub unit_id: u32,
1097}
1098
1099impl From<TertiaryDeviceAttributes> for ffi::DeviceAttributesTertiary {
1100    fn from(value: TertiaryDeviceAttributes) -> Self {
1101        Self {
1102            unit_id: value.unit_id,
1103        }
1104    }
1105}
1106
1107/// Color scheme reported in response to a CSI ? 996 n query.
1108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1109#[repr(u32)]
1110#[expect(missing_docs, reason = "self-explanatory")]
1111pub enum ColorScheme {
1112    Light = ffi::ColorScheme::LIGHT,
1113    Dark = ffi::ColorScheme::DARK,
1114}
1115
1116//---------------------------------------
1117// Callbacks
1118//---------------------------------------
1119
1120/// You might be wondering just what the heck this is.
1121///
1122/// Truth to be told, you don't need to understand how it works
1123/// in order to use it. It does a bunch of voodoo behind the scenes
1124/// that make sure all the invariants of the C API are upheld, while
1125/// providing a convenient API for Rust users.
1126///
1127/// Each handler is defined in this following format:
1128/// ```ignore
1129/// pub fn on_foobar(
1130///     &mut self,
1131///     // The corresponding GhosttyTerminalOption
1132///     tag = FOOBAR,
1133///
1134///     // The name of the original function type in C,
1135///     // along with the extra C parameters and the expected C return type
1136///     from = GhosttyTerminalFoobarFn(foo: *const u8, bar: usize) -> bool,
1137///
1138///     // The name of mapped Rust function type,
1139///     // along with the Rust parameters and return type.
1140///     //
1141///     // `<'t>` is used to tie the return value to the lifetime of the
1142///     // terminal. The name is arbitrary - any lifetime marker will do.
1143///     to = <'t>FoobarFn(&'t [u8]) -> bool,
1144/// ) |term, func| {
1145///     // `term` is the terminal and `func` is the Rust callback.
1146///     // Both names are arbitrary.
1147///
1148///     // Convert the raw parameters into Rust types.
1149///     // This is just to illustrate how.
1150///     let slice = unsafe { std::slice::from_raw_parts(foo, bar) };
1151///
1152///     // Call into user logic and return.
1153///     func(&terminal, slice)
1154/// }
1155/// ```
1156macro_rules! handlers {
1157    {
1158        $(
1159            $(#[$fmeta:meta])*
1160            $vis:vis fn $name:ident(
1161                &mut self,
1162                tag = $tag:ident,
1163                from = $rawfnty:ident( $($rfname:ident: $rfty:ty),*$(,)? ) $(-> $rawrty:ty)?,
1164                $(#[$tmeta:meta])*
1165                to = $(<$lf:lifetime>)? $fnty:ident( $($fty:ty),*$(,)? ) $(-> $rty:ty)?,
1166            ) |$t:ident, $func:ident| $block:block
1167        )*
1168    } => {
1169        /// Methods for registering [effect handlers](#effects).
1170        impl<'alloc, 'cb> $crate::terminal::Terminal<'alloc, 'cb> {$(
1171            $(#[$fmeta])*
1172            ///
1173            /// See [#Effects](Terminal#effects) for more details.
1174            $vis fn $name(&mut self, f: impl $fnty<'alloc, 'cb>) -> $crate::error::Result<&mut Self> {
1175                unsafe extern "C" fn callback(
1176                    t: $crate::ffi::Terminal,
1177                    ud: *mut std::ffi::c_void,
1178                    $($rfname: $rfty),*
1179                ) $(-> $rawrty)? {
1180                    // SAFETY: USERDATA is set to the boxed VTable pointee
1181                    // (derived from a mutable reference for write provenance)
1182                    // before the callback is registered. ghostty invokes
1183                    // callbacks synchronously during vt_write, so the VTable
1184                    // remains alive and exclusively accessed for the duration
1185                    // of this call.
1186                    let vtable = unsafe { &mut *ud.cast::<VTable<'_, '_>>() };
1187
1188                    let obj = $crate::alloc::Object::new(t).expect("received null terminal ptr in callback - this is a bug!");
1189                    // Build a temporary borrowed Terminal view for the callback
1190                    // without taking ownership of the underlying ghostty terminal.
1191                    let mut term = ::core::mem::ManuallyDrop::new($crate::terminal::Terminal::<'_, '_> {
1192                        inner: obj,
1193                        vtable: ::core::default::Default::default(),
1194                    });
1195                    let $t: &$crate::terminal::Terminal = &term;
1196                    let $func = vtable.$name.as_deref_mut()
1197                        .expect("no handler set but callback is still called - this is a bug!");
1198                    let ret = $block;
1199
1200                    // SAFETY: The temporary vtable was allocated solely to satisfy
1201                    // the Terminal layout expected by the callback signature. Drop
1202                    // it explicitly while intentionally leaving the borrowed
1203                    // terminal handle itself untouched.
1204                    unsafe { ::core::ptr::drop_in_place(&mut term.vtable) };
1205
1206                    ret
1207                }
1208
1209                self.vtable.$name = Some(::std::boxed::Box::new(f));
1210
1211                // USERDATA is a raw pointer option: pass the heap allocation
1212                // itself, not the address of the Box smart pointer field stored
1213                // inline in Terminal.
1214                //
1215                // Derive the pointer from a mutable reference so it carries
1216                // write provenance – the callback later reborrows it as &mut.
1217                let userdata = std::ptr::from_mut::<VTable<'alloc, 'cb>>(self.vtable.as_mut())
1218                    as *const ::std::ffi::c_void;
1219                self.set_ptr($crate::ffi::TerminalOption::USERDATA, userdata)?;
1220
1221                // The callback must be coerced into a function *pointer*
1222                // and not a function *item* (which is a ZST whose address is meaningless).
1223                // :)
1224                let callback_ptr: unsafe extern "C" fn(
1225                    $crate::ffi::Terminal,
1226                    *mut ::std::ffi::c_void,
1227                    $($rfty),*
1228                ) $(-> $rawrty)? = callback;
1229
1230                let result = unsafe {
1231                    $crate::ffi::ghostty_terminal_set(
1232                        self.inner.as_raw(),
1233                        $crate::ffi::TerminalOption::$tag,
1234                        callback_ptr as *const ::std::ffi::c_void
1235                    )
1236                };
1237                $crate::error::from_result(result)?;
1238                Ok(self)
1239            }
1240        )*}
1241        $(
1242            #[doc = concat!(
1243                "[Effect](Terminal#effects) callback type for [`Terminal::",
1244                stringify!($name),
1245                "`](Terminal::",
1246                stringify!($name),
1247                ").\n"
1248            )]
1249            $(#[$tmeta])*
1250            pub trait $fnty<'alloc, 'cb>:
1251                $(for<$lf>)? FnMut(
1252                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1253                    $($fty),*
1254                ) $(-> $rty)? + 'cb {}
1255
1256            impl<'alloc, 'cb, F> $fnty<'alloc, 'cb> for F
1257            where
1258                F: $(for<$lf>)? FnMut(
1259                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1260                    $($fty),*
1261                ) $(-> $rty)? + 'cb
1262            {}
1263        )*
1264
1265        struct VTable<'alloc, 'cb> {
1266            $($name: Option<::std::boxed::Box<dyn $fnty<'alloc, 'cb>>>),*
1267        }
1268
1269        impl ::core::fmt::Debug for VTable<'_, '_> {
1270            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
1271                f.write_str("VTable {..}")
1272            }
1273        }
1274
1275        impl ::core::default::Default for VTable<'_, '_> {
1276            fn default() -> Self {
1277                Self {
1278                    $($name: None),*
1279                }
1280            }
1281        }
1282    };
1283}
1284
1285handlers! {
1286    /// Call the given function when the terminal needs to write data back
1287    /// to the pty (e.g. in response to a DECRQM query or device status report).
1288    pub fn on_pty_write(
1289        &mut self,
1290        tag = WRITE_PTY,
1291        from = GhosttyTerminalWritePtyFn(ptr: *const u8, len: usize),
1292        to = <'t>PtyWriteFn(&'t [u8]),
1293    ) |term, func| {
1294        // SAFETY: We trust libghostty to return valid memory given we
1295        // uphold all lifetime invariants (e.g. no `vt_write` calls
1296        // during this callback, which is guaranteed via the mutable reference).
1297        let data = unsafe { std::slice::from_raw_parts(ptr, len) };
1298        func(&term, data);
1299    }
1300
1301    /// Call the given function when the terminal receives
1302    /// a BEL character (0x07).
1303    pub fn on_bell(
1304        &mut self,
1305        tag = BELL,
1306        from = GhosttyTerminalBellFn(),
1307        to = BellFn(),
1308    ) |term, func| {
1309        func(&term);
1310    }
1311
1312    /// Call the given function when the terminal receives
1313    /// an ENQ character (0x05).
1314    pub fn on_enquiry(
1315        &mut self,
1316        tag = ENQUIRY,
1317        from = GhosttyTerminalEnquiryFn() -> ffi::String,
1318        to = <'t>EnquiryFn() -> Option<&'t str>,
1319    ) |term, func| {
1320        func(&term).unwrap_or("").into()
1321    }
1322
1323    /// Call the given function when the terminal receives an XTVERSION
1324    /// query (CSI > q), and respond with the resulting version string
1325    /// (e.g. "myterm 1.0").
1326    pub fn on_xtversion(
1327        &mut self,
1328        tag = XTVERSION,
1329        from = GhosttyTerminalXtversionFn() -> ffi::String,
1330        to = <'t>XtversionFn() -> Option<&'t str>,
1331    ) |term, func| {
1332        func(&term).unwrap_or("").into()
1333    }
1334
1335    /// Call the given function when the terminal title changes
1336    /// via escape sequences (e.g. OSC 0 or OSC 2).
1337    ///
1338    /// The new title can be queried from the terminal after
1339    /// the callback returns.
1340    pub fn on_title_changed(
1341        &mut self,
1342        tag = TITLE_CHANGED,
1343        from = GhosttyTerminalTitleChangedFn(),
1344        to = TitleChangedFn(),
1345    ) |term, func| {
1346        func(&term);
1347    }
1348
1349    /// Call the given function when the terminal current working directory
1350    /// changes via escape sequences (e.g. OSC 7, OSC 9, or OSC 1337).
1351    ///
1352    /// The new working directory can be queried from the terminal after
1353    /// the callback returns.
1354    pub fn on_pwd_changed(
1355        &mut self,
1356        tag = PWD_CHANGED,
1357        from = GhosttyTerminalPwdChangedFn(),
1358        to = PwdChangedFn(),
1359    ) |term, func| {
1360        func(&term);
1361    }
1362
1363    /// Call the given function in response to XTWINOPS size queries
1364    /// (CSI 14/16/18 t).
1365    pub fn on_size(
1366        &mut self,
1367        tag = SIZE,
1368        from = GhosttyTerminalSizeFn(out: *mut ffi::SizeReportSize) -> bool,
1369        to = SizeFn() -> Option<SizeReportSize>,
1370    ) |term, func| {
1371        if let Some(size) = func(&term) {
1372            // SAFETY: Out pointer is assumed to be valid.
1373            unsafe { *out = size };
1374            true
1375        } else {
1376            false
1377        }
1378    }
1379
1380    /// Call the given function in response to a color scheme
1381    /// device status report query (CSI ? 996 n).
1382    ///
1383    /// Return `Some` to report the current color scheme,
1384    /// or return `None` to silently ignore.
1385    pub fn on_color_scheme(
1386        &mut self,
1387        tag = COLOR_SCHEME,
1388        from = GhosttyTerminalColorSchemeFn(out: *mut ffi::ColorScheme::Type) -> bool,
1389        to = ColorSchemeFn() -> Option<ColorScheme>,
1390    ) |term, func| {
1391        if let Some(size) = func(&term) {
1392            // SAFETY: Out pointer is assumed to be valid.
1393            unsafe { *out = size as ffi::ColorScheme::Type };
1394            true
1395        } else {
1396            false
1397        }
1398    }
1399
1400    /// Call the given function in response to a device attributes query
1401    /// (CSI c, CSI > c, or CSI = c).
1402    ///
1403    /// Return `Some` with the response data,
1404    /// or return `None` to silently ignore.
1405    pub fn on_device_attributes(
1406        &mut self,
1407        tag = DEVICE_ATTRIBUTES,
1408        from = GhosttyTerminalDeviceAttributesFn(out: *mut ffi::DeviceAttributes) -> bool,
1409        to = DeviceAttributesFn() -> Option<DeviceAttributes>,
1410    ) |term, func| {
1411        if let Some(size) = func(&term) {
1412            // SAFETY: Out pointer is assumed to be valid.
1413            unsafe { *out = size.into() };
1414            true
1415        } else {
1416            false
1417        }
1418    }
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423    use super::*;
1424    use crate::RenderState;
1425    use crate::render::CursorVisualStyle;
1426    use std::cell::{Cell, RefCell};
1427    use std::mem::ManuallyDrop;
1428
1429    #[inline(never)]
1430    fn build_terminal<'cb>(callback_count: &'cb RefCell<usize>) -> Terminal<'static, 'cb> {
1431        let mut terminal = Terminal::new(Options {
1432            cols: 80,
1433            rows: 24,
1434            max_scrollback: 1000,
1435        })
1436        .expect("terminal should initialize");
1437
1438        terminal
1439            .on_device_attributes(move |_term| {
1440                *callback_count.borrow_mut() += 1;
1441                Some(DeviceAttributes {
1442                    primary: PrimaryDeviceAttributes::new(
1443                        ConformanceLevel::VT220,
1444                        &[DeviceAttributeFeature::ANSI_COLOR],
1445                    ),
1446                    secondary: SecondaryDeviceAttributes {
1447                        device_type: DeviceType::VT220,
1448                        firmware_version: 1,
1449                        rom_cartridge: 0,
1450                    },
1451                    tertiary: TertiaryDeviceAttributes { unit_id: 0 },
1452                })
1453            })
1454            .expect("callback should register");
1455
1456        terminal
1457    }
1458
1459    /// Move a value into distinct heap storage with an explicit byte-for-byte
1460    /// relocation so the test does not rely on optimizer or allocator behavior.
1461    fn relocate_into_new_box<T>(value: T) -> (Box<T>, usize, usize) {
1462        // Keep the source allocation alive without running T's destructor.
1463        // We need the bytes to remain initialized until after the copy.
1464        let src = Box::new(ManuallyDrop::new(value));
1465        let src_addr = std::ptr::from_ref(&**src).cast::<T>() as usize;
1466
1467        unsafe {
1468            let dst_layout = std::alloc::Layout::new::<T>();
1469            let dst_ptr = std::alloc::alloc(dst_layout).cast::<T>();
1470            if dst_ptr.is_null() {
1471                std::alloc::handle_alloc_error(dst_layout);
1472            }
1473
1474            let dst_addr = dst_ptr as usize;
1475            assert_ne!(
1476                src_addr, dst_addr,
1477                "test setup failed: source and destination storage unexpectedly match"
1478            );
1479
1480            // SAFETY: src points to a fully initialized T wrapped in
1481            // ManuallyDrop, dst points to distinct uninitialized storage for
1482            // exactly one T, and the regions do not overlap.
1483            std::ptr::copy_nonoverlapping(std::ptr::from_ref(&**src).cast::<T>(), dst_ptr, 1);
1484
1485            // SAFETY: src was allocated as Box<ManuallyDrop<T>> and must be
1486            // freed without dropping T because ownership was transferred by
1487            // the raw byte copy above.
1488            std::alloc::dealloc(
1489                Box::into_raw(src).cast::<u8>(),
1490                std::alloc::Layout::new::<ManuallyDrop<T>>(),
1491            );
1492
1493            // SAFETY: We just initialized dst_ptr by copying a valid T into it,
1494            // so it now owns exactly one initialized T allocation.
1495            (Box::from_raw(dst_ptr), src_addr, dst_addr)
1496        }
1497    }
1498
1499    /// Send an OSC 2 title sequence, then verify `term.title()` returns the
1500    /// correct value inside the `on_title_changed` callback.
1501    #[test]
1502    fn title_changed_callback_returns_correct_title() {
1503        // The callback bound on `on_title_changed` is `'cb`, not `'static`,
1504        // so the closure can borrow stack locals directly – no Rc needed.
1505        let captured_title: RefCell<String> = RefCell::new(String::new());
1506        let callback_count: Cell<usize> = Cell::new(0);
1507
1508        let mut terminal = Terminal::new(Options {
1509            cols: 80,
1510            rows: 24,
1511            max_scrollback: 0,
1512        })
1513        .expect("terminal should initialize");
1514
1515        terminal
1516            .on_title_changed(|term| {
1517                callback_count.set(callback_count.get() + 1);
1518                let title = term
1519                    .title()
1520                    .expect("title() should succeed inside callback");
1521                *captured_title.borrow_mut() = title.to_owned();
1522            })
1523            .expect("callback should register");
1524
1525        // OSC 2 (set title) should invoke on_title_changed.
1526        terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
1527        assert_eq!(callback_count.get(), 1);
1528        assert_eq!(*captured_title.borrow(), "Hello Effects");
1529
1530        // A second title change should fire the callback again.
1531        terminal.vt_write(b"\x1b]2;Second Title\x1b\\");
1532        assert_eq!(callback_count.get(), 2);
1533        assert_eq!(*captured_title.borrow(), "Second Title");
1534    }
1535
1536    /// Send an OSC 7 current-directory sequence, then verify `term.pwd()`
1537    /// returns the correct value inside the `on_pwd_changed` callback.
1538    #[test]
1539    fn pwd_changed_callback_returns_correct_pwd() {
1540        let captured_pwd: RefCell<String> = RefCell::new(String::new());
1541        let callback_count: Cell<usize> = Cell::new(0);
1542
1543        let mut terminal = Terminal::new(Options {
1544            cols: 80,
1545            rows: 24,
1546            max_scrollback: 0,
1547        })
1548        .expect("terminal should initialize");
1549
1550        terminal
1551            .on_pwd_changed(|term| {
1552                callback_count.set(callback_count.get() + 1);
1553                let pwd = term.pwd().expect("pwd() should succeed inside callback");
1554                *captured_pwd.borrow_mut() = pwd.to_owned();
1555            })
1556            .expect("callback should register");
1557
1558        terminal.vt_write(b"\x1b]7;file://localhost/tmp/project\x1b\\");
1559        assert_eq!(callback_count.get(), 1);
1560        assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/project");
1561
1562        terminal.vt_write(b"\x1b]7;file://localhost/tmp/other\x1b\\");
1563        assert_eq!(callback_count.get(), 2);
1564        assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/other");
1565    }
1566
1567    #[test]
1568    fn default_cursor_reset_uses_configured_style_and_blink() {
1569        let mut terminal = Terminal::new(Options {
1570            cols: 80,
1571            rows: 24,
1572            max_scrollback: 0,
1573        })
1574        .expect("terminal should initialize");
1575        let mut render_state = RenderState::new().expect("render state should initialize");
1576
1577        terminal
1578            .set_default_cursor_style(Some(CursorStyle::Underline))
1579            .expect("default cursor style should update")
1580            .set_default_cursor_blink(Some(true))
1581            .expect("default cursor blink should update");
1582
1583        terminal.vt_write(b"\x1b[0 q");
1584        let snapshot = render_state
1585            .update(&terminal)
1586            .expect("render state should update");
1587
1588        assert_eq!(
1589            snapshot
1590                .cursor_visual_style()
1591                .expect("cursor style should be readable"),
1592            CursorVisualStyle::Underline
1593        );
1594        assert!(
1595            snapshot
1596                .cursor_blinking()
1597                .expect("cursor blink should be readable")
1598        );
1599    }
1600
1601    #[test]
1602    fn glyph_protocol_enabled_setting_updates() {
1603        let mut terminal = Terminal::new(Options {
1604            cols: 80,
1605            rows: 24,
1606            max_scrollback: 0,
1607        })
1608        .expect("terminal should initialize");
1609
1610        terminal
1611            .set_glyph_protocol_enabled(false)
1612            .expect("glyph protocol should disable")
1613            .set_glyph_protocol_enabled(true)
1614            .expect("glyph protocol should enable");
1615    }
1616
1617    /// Explicitly relocate the Terminal into distinct storage, then verify the
1618    /// callback still fires through the stable VTable userdata pointer.
1619    #[test]
1620    fn callbacks_survive_explicit_relocation() {
1621        let callback_count = RefCell::new(0usize);
1622        let terminal = build_terminal(&callback_count);
1623        let (mut terminal, addr_before, addr_after) = relocate_into_new_box(terminal);
1624        assert_ne!(addr_before, addr_after);
1625
1626        // Primary DA request (CSI c) should invoke on_device_attributes.
1627        terminal.vt_write(b"\x1b[c");
1628        assert_eq!(*callback_count.borrow(), 1);
1629    }
1630
1631    fn tiny_terminal() -> Terminal<'static, 'static> {
1632        Terminal::new(Options {
1633            cols: 8,
1634            rows: 3,
1635            max_scrollback: 100,
1636        })
1637        .expect("terminal should initialize")
1638    }
1639
1640    fn codepoint_at_tracked_ref(terminal: &Terminal<'_, '_>, tracked: &TrackedGridRef) -> u32 {
1641        let snapshot = tracked
1642            .snapshot(terminal)
1643            .expect("tracked snapshot should not fail")
1644            .expect("tracked ref should have a value");
1645        snapshot
1646            .cell()
1647            .expect("tracked snapshot should resolve to a cell")
1648            .codepoint()
1649            .expect("tracked snapshot cell should expose a codepoint")
1650    }
1651
1652    #[test]
1653    fn tracked_grid_ref_follows_scroll() {
1654        let mut terminal = tiny_terminal();
1655        terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1656
1657        let tracked = terminal
1658            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1659            .expect("tracked grid ref should initialize");
1660
1661        terminal.vt_write(b"\r\ndelta");
1662
1663        assert!(tracked.has_value());
1664        assert_eq!(
1665            codepoint_at_tracked_ref(&terminal, &tracked),
1666            u32::from('a')
1667        );
1668        assert_eq!(
1669            tracked
1670                .point(PointSpace::Screen)
1671                .expect("tracked point should resolve")
1672                .expect("tracked point should have a value")
1673                .x,
1674            0
1675        );
1676    }
1677
1678    #[test]
1679    fn tracked_grid_ref_reports_loss_and_can_set_point() {
1680        let mut terminal = tiny_terminal();
1681        terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1682
1683        let mut tracked = terminal
1684            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1685            .expect("tracked grid ref should initialize");
1686
1687        terminal.reset();
1688
1689        assert!(!tracked.has_value());
1690        assert!(
1691            tracked
1692                .snapshot(&terminal)
1693                .expect("missing tracked snapshot should not fail")
1694                .is_none()
1695        );
1696        assert!(
1697            tracked
1698                .point(PointSpace::Screen)
1699                .expect("missing tracked point should not fail")
1700                .is_none()
1701        );
1702
1703        terminal.vt_write(b"echo");
1704        tracked
1705            .set(&mut terminal, Point::Active(PointCoordinate { x: 0, y: 0 }))
1706            .expect("tracked grid ref should set to a new point");
1707
1708        assert!(tracked.has_value());
1709        assert_eq!(
1710            codepoint_at_tracked_ref(&terminal, &tracked),
1711            u32::from('e')
1712        );
1713    }
1714
1715    #[test]
1716    fn tracked_grid_ref_survives_terminal_drop() {
1717        let tracked = {
1718            let mut terminal = tiny_terminal();
1719            terminal.vt_write(b"alpha");
1720            terminal
1721                .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1722                .expect("tracked grid ref should initialize")
1723        };
1724
1725        assert!(!tracked.has_value());
1726        assert!(
1727            tracked
1728                .point(PointSpace::Screen)
1729                .expect("detached tracked point should not fail")
1730                .is_none()
1731        );
1732    }
1733
1734    #[test]
1735    fn tracked_grid_ref_rejects_different_terminal() {
1736        let mut first = tiny_terminal();
1737        first.vt_write(b"alpha");
1738        let mut second = tiny_terminal();
1739        second.vt_write(b"bravo");
1740
1741        let mut tracked = first
1742            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1743            .expect("tracked grid ref should initialize");
1744
1745        assert!(matches!(
1746            tracked.snapshot(&second),
1747            Err(Error::InvalidValue)
1748        ));
1749        assert!(matches!(
1750            tracked.set(&mut second, Point::Active(PointCoordinate { x: 0, y: 0 })),
1751            Err(Error::InvalidValue)
1752        ));
1753    }
1754
1755    #[test]
1756    fn grid_ref_converts_back_to_point() {
1757        let mut terminal = tiny_terminal();
1758        terminal.vt_write(b"alpha");
1759
1760        let original = PointCoordinate { x: 1, y: 0 };
1761        let grid_ref = terminal
1762            .grid_ref(Point::Active(original))
1763            .expect("grid ref should resolve");
1764
1765        assert_eq!(
1766            terminal
1767                .point_from_grid_ref(&grid_ref, PointSpace::Active)
1768                .expect("grid ref point conversion should not fail")
1769                .expect("grid ref should be representable in active space"),
1770            original
1771        );
1772    }
1773}