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 { std::slice::from_raw_parts(str.ptr, str.len) };
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 { std::slice::from_raw_parts(str.ptr, str.len) };
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    pub fn contents(&self) -> ClipboardContents<'t> {
1268        // SAFETY: We trust libghostty to give us a valid pointer and length
1269        // within the lifetime of the callback.
1270        ClipboardContents(unsafe {
1271            std::slice::from_raw_parts((*self.ptr).contents, (*self.ptr).contents_len).iter()
1272        })
1273    }
1274}
1275
1276/// An iterator into a borrowed array of MIME representations.
1277#[derive(Clone, Debug)]
1278pub struct ClipboardContents<'t>(std::slice::Iter<'t, ffi::ClipboardContent>);
1279
1280impl<'t> Iterator for ClipboardContents<'t> {
1281    type Item = ClipboardContent<'t>;
1282
1283    fn next(&mut self) -> Option<Self::Item> {
1284        self.0
1285            .next()
1286            .map(|v| unsafe { ClipboardContent::from_raw(v) })
1287    }
1288    fn size_hint(&self) -> (usize, Option<usize>) {
1289        self.0.size_hint()
1290    }
1291}
1292impl DoubleEndedIterator for ClipboardContents<'_> {
1293    fn next_back(&mut self) -> Option<Self::Item> {
1294        self.0
1295            .next_back()
1296            .map(|v| unsafe { ClipboardContent::from_raw(v) })
1297    }
1298}
1299impl ExactSizeIterator for ClipboardContents<'_> {}
1300impl std::iter::FusedIterator for ClipboardContents<'_> {}
1301
1302/// One MIME representation in a clipboard write.
1303///
1304/// The data is binary-safe and has already been decoded from any protocol-level
1305/// encoding. A zero-length data string is an explicit empty representation; it
1306/// does not clear the clipboard.
1307#[derive(Clone, Copy, Debug)]
1308pub struct ClipboardContent<'t> {
1309    /// MIME type of the representation.
1310    pub mime: &'t str,
1311    /// Decoded, binary-safe representation data.
1312    pub data: &'t str,
1313}
1314impl<'t> ClipboardContent<'t> {
1315    /// # Safety
1316    ///
1317    /// Caller must guarantee that the given raw value is valid within
1318    /// the given lifetime.
1319    unsafe fn from_raw(value: &ffi::ClipboardContent) -> Self {
1320        // SAFETY: Upheld by caller
1321        unsafe {
1322            Self {
1323                mime: value.mime.to_str(),
1324                data: value.data.to_str(),
1325            }
1326        }
1327    }
1328}
1329
1330/// Clipboard destination for a clipboard write.
1331///
1332/// Protocol-specific destination identifiers are normalized to these values
1333/// before the clipboard write callback is invoked.
1334#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
1335#[repr(u32)]
1336pub enum ClipboardLocation {
1337    /// The standard system clipboard.
1338    Standard = ffi::ClipboardLocation::STANDARD,
1339    /// The selection clipboard.
1340    Selection = ffi::ClipboardLocation::SELECTION,
1341    /// The primary selection clipboard.
1342    Primary = ffi::ClipboardLocation::PRIMARY,
1343}
1344
1345/// Possible errors caused by a clipboard write callback.
1346///
1347/// Protocols without write acknowledgements, including OSC 52 and iTerm2
1348/// OSC 1337 Copy, ignore this result.
1349#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
1350#[repr(u32)]
1351pub enum ClipboardWriteError {
1352    /// The clipboard write was denied by policy or the user.
1353    Denied = ffi::ClipboardWriteResult::DENIED,
1354    /// The destination or one or more representations are unsupported.
1355    Unsupported = ffi::ClipboardWriteResult::UNSUPPORTED,
1356    /// The clipboard is temporarily unavailable.
1357    Busy = ffi::ClipboardWriteResult::BUSY,
1358    /// One or more representations contain invalid data.
1359    InvalidData = ffi::ClipboardWriteResult::INVALID_DATA,
1360    /// The clipboard write failed due to an I/O error.
1361    IoError = ffi::ClipboardWriteResult::IO_ERROR,
1362}
1363
1364//---------------------------------------
1365// Callbacks
1366//---------------------------------------
1367
1368/// You might be wondering just what the heck this is.
1369///
1370/// Truth to be told, you don't need to understand how it works
1371/// in order to use it. It does a bunch of voodoo behind the scenes
1372/// that make sure all the invariants of the C API are upheld, while
1373/// providing a convenient API for Rust users.
1374///
1375/// Each handler is defined in this following format:
1376/// ```ignore
1377/// pub fn on_foobar(
1378///     &mut self,
1379///     // The corresponding GhosttyTerminalOption
1380///     tag = FOOBAR,
1381///
1382///     // The name of the original function type in C,
1383///     // along with the extra C parameters and the expected C return type
1384///     from = GhosttyTerminalFoobarFn(foo: *const u8, bar: usize) -> bool,
1385///
1386///     // The name of mapped Rust function type,
1387///     // along with the Rust parameters and return type.
1388///     //
1389///     // `<'t>` is used to tie the return value to the lifetime of the
1390///     // terminal. The name is arbitrary - any lifetime marker will do.
1391///     to = <'t>FoobarFn(&'t [u8]) -> bool,
1392/// ) |term, func| {
1393///     // `term` is the terminal and `func` is the Rust callback.
1394///     // Both names are arbitrary.
1395///
1396///     // Convert the raw parameters into Rust types.
1397///     // This is just to illustrate how.
1398///     let slice = unsafe { std::slice::from_raw_parts(foo, bar) };
1399///
1400///     // Call into user logic and return.
1401///     func(&terminal, slice)
1402/// }
1403/// ```
1404macro_rules! handlers {
1405    {
1406        $(
1407            $(#[$fmeta:meta])*
1408            $vis:vis fn $name:ident(
1409                &mut self,
1410                tag = $tag:ident,
1411                from = $rawfnty:ident( $($rfname:ident: $rfty:ty),*$(,)? ) $(-> $rawrty:ty)?,
1412                $(#[$tmeta:meta])*
1413                to = $(<$lf:lifetime>)? $fnty:ident( $($fty:ty),*$(,)? ) $(-> $rty:ty)?,
1414            ) |$t:ident, $func:ident| $block:block
1415        )*
1416    } => {
1417        /// Methods for registering [effect handlers](#effects).
1418        impl<'alloc, 'cb> $crate::terminal::Terminal<'alloc, 'cb> {$(
1419            $(#[$fmeta])*
1420            ///
1421            /// See [#Effects](Terminal#effects) for more details.
1422            $vis fn $name(&mut self, f: impl $fnty<'alloc, 'cb>) -> $crate::error::Result<&mut Self> {
1423                unsafe extern "C" fn callback(
1424                    t: $crate::ffi::Terminal,
1425                    ud: *mut std::ffi::c_void,
1426                    $($rfname: $rfty),*
1427                ) $(-> $rawrty)? {
1428                    // SAFETY: USERDATA is set to the boxed VTable pointee
1429                    // (derived from a mutable reference for write provenance)
1430                    // before the callback is registered. ghostty invokes
1431                    // callbacks synchronously during vt_write, so the VTable
1432                    // remains alive and exclusively accessed for the duration
1433                    // of this call.
1434                    let vtable = unsafe { &mut *ud.cast::<VTable<'_, '_>>() };
1435
1436                    let obj = $crate::alloc::Object::new(t).expect("received null terminal ptr in callback - this is a bug!");
1437                    // Build a temporary borrowed Terminal view for the callback
1438                    // without taking ownership of the underlying ghostty terminal.
1439                    let mut term = ::core::mem::ManuallyDrop::new($crate::terminal::Terminal::<'_, '_> {
1440                        inner: obj,
1441                        vtable: ::core::default::Default::default(),
1442                    });
1443                    let $t: &$crate::terminal::Terminal = &term;
1444                    let $func = vtable.$name.as_deref_mut()
1445                        .expect("no handler set but callback is still called - this is a bug!");
1446                    let ret = $block;
1447
1448                    // SAFETY: The temporary vtable was allocated solely to satisfy
1449                    // the Terminal layout expected by the callback signature. Drop
1450                    // it explicitly while intentionally leaving the borrowed
1451                    // terminal handle itself untouched.
1452                    unsafe { ::core::ptr::drop_in_place(&mut term.vtable) };
1453
1454                    ret
1455                }
1456
1457                self.vtable.$name = Some(::std::boxed::Box::new(f));
1458
1459                // USERDATA is a raw pointer option: pass the heap allocation
1460                // itself, not the address of the Box smart pointer field stored
1461                // inline in Terminal.
1462                //
1463                // Derive the pointer from a mutable reference so it carries
1464                // write provenance – the callback later reborrows it as &mut.
1465                let userdata = std::ptr::from_mut::<VTable<'alloc, 'cb>>(self.vtable.as_mut())
1466                    as *const ::std::ffi::c_void;
1467                self.set_ptr($crate::ffi::TerminalOption::USERDATA, userdata)?;
1468
1469                // The callback must be coerced into a function *pointer*
1470                // and not a function *item* (which is a ZST whose address is meaningless).
1471                // :)
1472                let callback_ptr: unsafe extern "C" fn(
1473                    $crate::ffi::Terminal,
1474                    *mut ::std::ffi::c_void,
1475                    $($rfty),*
1476                ) $(-> $rawrty)? = callback;
1477
1478                let result = unsafe {
1479                    $crate::ffi::ghostty_terminal_set(
1480                        self.inner.as_raw(),
1481                        $crate::ffi::TerminalOption::$tag,
1482                        callback_ptr as *const ::std::ffi::c_void
1483                    )
1484                };
1485                $crate::error::from_result(result)?;
1486                Ok(self)
1487            }
1488        )*}
1489        $(
1490            #[doc = concat!(
1491                "[Effect](Terminal#effects) callback type for [`Terminal::",
1492                stringify!($name),
1493                "`](Terminal::",
1494                stringify!($name),
1495                ").\n"
1496            )]
1497            $(#[$tmeta])*
1498            pub trait $fnty<'alloc, 'cb>:
1499                $(for<$lf>)? FnMut(
1500                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1501                    $($fty),*
1502                ) $(-> $rty)? + 'cb {}
1503
1504            impl<'alloc, 'cb, F> $fnty<'alloc, 'cb> for F
1505            where
1506                F: $(for<$lf>)? FnMut(
1507                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1508                    $($fty),*
1509                ) $(-> $rty)? + 'cb
1510            {}
1511        )*
1512
1513        struct VTable<'alloc, 'cb> {
1514            $($name: Option<::std::boxed::Box<dyn $fnty<'alloc, 'cb>>>),*
1515        }
1516
1517        impl ::core::fmt::Debug for VTable<'_, '_> {
1518            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
1519                f.write_str("VTable {..}")
1520            }
1521        }
1522
1523        impl ::core::default::Default for VTable<'_, '_> {
1524            fn default() -> Self {
1525                Self {
1526                    $($name: None),*
1527                }
1528            }
1529        }
1530    };
1531}
1532
1533handlers! {
1534    /// Call the given function when the terminal needs to write data back
1535    /// to the pty (e.g. in response to a DECRQM query or device status report).
1536    pub fn on_pty_write(
1537        &mut self,
1538        tag = WRITE_PTY,
1539        from = GhosttyTerminalWritePtyFn(ptr: *const u8, len: usize),
1540        to = <'t>PtyWriteFn(&'t [u8]),
1541    ) |term, func| {
1542        // SAFETY: We trust libghostty to return valid memory given we
1543        // uphold all lifetime invariants (e.g. no `vt_write` calls
1544        // during this callback, which is guaranteed via the mutable reference).
1545        let data = unsafe { std::slice::from_raw_parts(ptr, len) };
1546        func(&term, data);
1547    }
1548
1549    /// Call the given function when the terminal receives
1550    /// a BEL character (0x07).
1551    pub fn on_bell(
1552        &mut self,
1553        tag = BELL,
1554        from = GhosttyTerminalBellFn(),
1555        to = BellFn(),
1556    ) |term, func| {
1557        func(&term);
1558    }
1559
1560    /// Call the given function when the terminal receives
1561    /// an ENQ character (0x05).
1562    pub fn on_enquiry(
1563        &mut self,
1564        tag = ENQUIRY,
1565        from = GhosttyTerminalEnquiryFn() -> ffi::String,
1566        to = <'t>EnquiryFn() -> Option<&'t str>,
1567    ) |term, func| {
1568        func(&term).unwrap_or("").into()
1569    }
1570
1571    /// Call the given function when the terminal receives an XTVERSION
1572    /// query (CSI > q), and respond with the resulting version string
1573    /// (e.g. "myterm 1.0").
1574    pub fn on_xtversion(
1575        &mut self,
1576        tag = XTVERSION,
1577        from = GhosttyTerminalXtversionFn() -> ffi::String,
1578        to = <'t>XtversionFn() -> Option<&'t str>,
1579    ) |term, func| {
1580        func(&term).unwrap_or("").into()
1581    }
1582
1583    /// Call the given function when the terminal title changes
1584    /// via escape sequences (e.g. OSC 0 or OSC 2).
1585    ///
1586    /// The new title can be queried from the terminal after
1587    /// the callback returns.
1588    pub fn on_title_changed(
1589        &mut self,
1590        tag = TITLE_CHANGED,
1591        from = GhosttyTerminalTitleChangedFn(),
1592        to = TitleChangedFn(),
1593    ) |term, func| {
1594        func(&term);
1595    }
1596
1597    /// Call the given function when the terminal current working directory
1598    /// changes via escape sequences (e.g. OSC 7, OSC 9, or OSC 1337).
1599    ///
1600    /// The new working directory can be queried from the terminal after
1601    /// the callback returns.
1602    pub fn on_pwd_changed(
1603        &mut self,
1604        tag = PWD_CHANGED,
1605        from = GhosttyTerminalPwdChangedFn(),
1606        to = PwdChangedFn(),
1607    ) |term, func| {
1608        func(&term);
1609    }
1610
1611    /// Call the given function in response to XTWINOPS size queries
1612    /// (CSI 14/16/18 t).
1613    pub fn on_size(
1614        &mut self,
1615        tag = SIZE,
1616        from = GhosttyTerminalSizeFn(out: *mut ffi::SizeReportSize) -> bool,
1617        to = SizeFn() -> Option<SizeReportSize>,
1618    ) |term, func| {
1619        if let Some(size) = func(&term) {
1620            // SAFETY: Out pointer is assumed to be valid.
1621            unsafe { *out = size };
1622            true
1623        } else {
1624            false
1625        }
1626    }
1627
1628    /// Call the given function in response to a color scheme
1629    /// device status report query (CSI ? 996 n).
1630    ///
1631    /// Return `Some` to report the current color scheme,
1632    /// or return `None` to silently ignore.
1633    pub fn on_color_scheme(
1634        &mut self,
1635        tag = COLOR_SCHEME,
1636        from = GhosttyTerminalColorSchemeFn(out: *mut ffi::ColorScheme::Type) -> bool,
1637        to = ColorSchemeFn() -> Option<ColorScheme>,
1638    ) |term, func| {
1639        if let Some(size) = func(&term) {
1640            // SAFETY: Out pointer is assumed to be valid.
1641            unsafe { *out = size.into() };
1642            true
1643        } else {
1644            false
1645        }
1646    }
1647
1648    /// Call the given function in response to a device attributes query
1649    /// (CSI c, CSI > c, or CSI = c).
1650    ///
1651    /// Return `Some` with the response data,
1652    /// or return `None` to silently ignore.
1653    pub fn on_device_attributes(
1654        &mut self,
1655        tag = DEVICE_ATTRIBUTES,
1656        from = GhosttyTerminalDeviceAttributesFn(out: *mut ffi::DeviceAttributes) -> bool,
1657        to = DeviceAttributesFn() -> Option<DeviceAttributes>,
1658    ) |term, func| {
1659        if let Some(size) = func(&term) {
1660            // SAFETY: Out pointer is assumed to be valid.
1661            unsafe { *out = size.into() };
1662            true
1663        } else {
1664            false
1665        }
1666    }
1667
1668    /// Call the given function when the running program performs a clipboard write.
1669    ///
1670    ///
1671    /// Protocol details such as OSC 52 selectors, base64 encoding, multipart
1672    /// chunks, aliases, and terminators are normalized before this callback is
1673    /// invoked. OSC 52 and iTerm2 OSC 1337 Copy writes therefore use the same
1674    /// callback shape.
1675    ///
1676    /// OSC 52 clipboard read requests (\"?\") are always ignored and never
1677    /// forwarded to this callback.
1678    pub fn on_clipboard_write(
1679        &mut self,
1680        tag = CLIPBOARD_WRITE,
1681        from = GhosttyTerminalClipboardWriteFn(
1682            write: *const ffi::ClipboardWrite
1683        ) -> ffi::ClipboardWriteResult::Type,
1684        to = <'t>ClipboardWriteFn(ClipboardWrite<'t>) -> std::result::Result<(), ClipboardWriteError>,
1685    ) |term, func| {
1686        match func(&term, unsafe { ClipboardWrite::from_raw(write) }) {
1687            Ok(_) => ffi::ClipboardWriteResult::SUCCESS,
1688            Err(e) => e.into()
1689        }
1690    }
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695    use super::*;
1696    use crate::RenderState;
1697    use crate::render::CursorVisualStyle;
1698    use std::cell::{Cell, RefCell};
1699    use std::mem::ManuallyDrop;
1700
1701    #[inline(never)]
1702    fn build_terminal<'cb>(callback_count: &'cb RefCell<usize>) -> Terminal<'static, 'cb> {
1703        let mut terminal = Terminal::new(Options {
1704            cols: 80,
1705            rows: 24,
1706            max_scrollback: 1000,
1707        })
1708        .expect("terminal should initialize");
1709
1710        terminal
1711            .on_device_attributes(move |_term| {
1712                *callback_count.borrow_mut() += 1;
1713                Some(DeviceAttributes {
1714                    primary: PrimaryDeviceAttributes::new(
1715                        ConformanceLevel::VT220,
1716                        &[DeviceAttributeFeature::ANSI_COLOR],
1717                    ),
1718                    secondary: SecondaryDeviceAttributes {
1719                        device_type: DeviceType::VT220,
1720                        firmware_version: 1,
1721                        rom_cartridge: 0,
1722                    },
1723                    tertiary: TertiaryDeviceAttributes { unit_id: 0 },
1724                })
1725            })
1726            .expect("callback should register");
1727
1728        terminal
1729    }
1730
1731    /// Move a value into distinct heap storage with an explicit byte-for-byte
1732    /// relocation so the test does not rely on optimizer or allocator behavior.
1733    fn relocate_into_new_box<T>(value: T) -> (Box<T>, usize, usize) {
1734        // Keep the source allocation alive without running T's destructor.
1735        // We need the bytes to remain initialized until after the copy.
1736        let src = Box::new(ManuallyDrop::new(value));
1737        let src_addr = std::ptr::from_ref(&**src).cast::<T>() as usize;
1738
1739        unsafe {
1740            let dst_layout = std::alloc::Layout::new::<T>();
1741            let dst_ptr = std::alloc::alloc(dst_layout).cast::<T>();
1742            if dst_ptr.is_null() {
1743                std::alloc::handle_alloc_error(dst_layout);
1744            }
1745
1746            let dst_addr = dst_ptr as usize;
1747            assert_ne!(
1748                src_addr, dst_addr,
1749                "test setup failed: source and destination storage unexpectedly match"
1750            );
1751
1752            // SAFETY: src points to a fully initialized T wrapped in
1753            // ManuallyDrop, dst points to distinct uninitialized storage for
1754            // exactly one T, and the regions do not overlap.
1755            std::ptr::copy_nonoverlapping(std::ptr::from_ref(&**src).cast::<T>(), dst_ptr, 1);
1756
1757            // SAFETY: src was allocated as Box<ManuallyDrop<T>> and must be
1758            // freed without dropping T because ownership was transferred by
1759            // the raw byte copy above.
1760            std::alloc::dealloc(
1761                Box::into_raw(src).cast::<u8>(),
1762                std::alloc::Layout::new::<ManuallyDrop<T>>(),
1763            );
1764
1765            // SAFETY: We just initialized dst_ptr by copying a valid T into it,
1766            // so it now owns exactly one initialized T allocation.
1767            (Box::from_raw(dst_ptr), src_addr, dst_addr)
1768        }
1769    }
1770
1771    /// Send an OSC 2 title sequence, then verify `term.title()` returns the
1772    /// correct value inside the `on_title_changed` callback.
1773    #[test]
1774    fn title_changed_callback_returns_correct_title() {
1775        // The callback bound on `on_title_changed` is `'cb`, not `'static`,
1776        // so the closure can borrow stack locals directly – no Rc needed.
1777        let captured_title: RefCell<String> = RefCell::new(String::new());
1778        let callback_count: Cell<usize> = Cell::new(0);
1779
1780        let mut terminal = Terminal::new(Options {
1781            cols: 80,
1782            rows: 24,
1783            max_scrollback: 0,
1784        })
1785        .expect("terminal should initialize");
1786
1787        terminal
1788            .on_title_changed(|term| {
1789                callback_count.set(callback_count.get() + 1);
1790                let title = term
1791                    .title()
1792                    .expect("title() should succeed inside callback");
1793                *captured_title.borrow_mut() = title.to_owned();
1794            })
1795            .expect("callback should register");
1796
1797        // OSC 2 (set title) should invoke on_title_changed.
1798        terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
1799        assert_eq!(callback_count.get(), 1);
1800        assert_eq!(*captured_title.borrow(), "Hello Effects");
1801
1802        // A second title change should fire the callback again.
1803        terminal.vt_write(b"\x1b]2;Second Title\x1b\\");
1804        assert_eq!(callback_count.get(), 2);
1805        assert_eq!(*captured_title.borrow(), "Second Title");
1806    }
1807
1808    /// Send an OSC 7 current-directory sequence, then verify `term.pwd()`
1809    /// returns the correct value inside the `on_pwd_changed` callback.
1810    #[test]
1811    fn pwd_changed_callback_returns_correct_pwd() {
1812        let captured_pwd: RefCell<String> = RefCell::new(String::new());
1813        let callback_count: Cell<usize> = Cell::new(0);
1814
1815        let mut terminal = Terminal::new(Options {
1816            cols: 80,
1817            rows: 24,
1818            max_scrollback: 0,
1819        })
1820        .expect("terminal should initialize");
1821
1822        terminal
1823            .on_pwd_changed(|term| {
1824                callback_count.set(callback_count.get() + 1);
1825                let pwd = term.pwd().expect("pwd() should succeed inside callback");
1826                *captured_pwd.borrow_mut() = pwd.to_owned();
1827            })
1828            .expect("callback should register");
1829
1830        terminal.vt_write(b"\x1b]7;file://localhost/tmp/project\x1b\\");
1831        assert_eq!(callback_count.get(), 1);
1832        assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/project");
1833
1834        terminal.vt_write(b"\x1b]7;file://localhost/tmp/other\x1b\\");
1835        assert_eq!(callback_count.get(), 2);
1836        assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/other");
1837    }
1838
1839    #[test]
1840    fn default_cursor_reset_uses_configured_style_and_blink() {
1841        let mut terminal = Terminal::new(Options {
1842            cols: 80,
1843            rows: 24,
1844            max_scrollback: 0,
1845        })
1846        .expect("terminal should initialize");
1847        let mut render_state = RenderState::new().expect("render state should initialize");
1848
1849        terminal
1850            .set_default_cursor_style(Some(CursorStyle::Underline))
1851            .expect("default cursor style should update")
1852            .set_default_cursor_blink(Some(true))
1853            .expect("default cursor blink should update");
1854
1855        terminal.vt_write(b"\x1b[0 q");
1856        let snapshot = render_state
1857            .update(&terminal)
1858            .expect("render state should update");
1859
1860        assert_eq!(
1861            snapshot
1862                .cursor_visual_style()
1863                .expect("cursor style should be readable"),
1864            CursorVisualStyle::Underline
1865        );
1866        assert!(
1867            snapshot
1868                .cursor_blinking()
1869                .expect("cursor blink should be readable")
1870        );
1871    }
1872
1873    #[test]
1874    fn glyph_protocol_enabled_setting_updates() {
1875        let mut terminal = Terminal::new(Options {
1876            cols: 80,
1877            rows: 24,
1878            max_scrollback: 0,
1879        })
1880        .expect("terminal should initialize");
1881
1882        terminal
1883            .set_glyph_protocol_enabled(false)
1884            .expect("glyph protocol should disable")
1885            .set_glyph_protocol_enabled(true)
1886            .expect("glyph protocol should enable");
1887    }
1888
1889    /// Explicitly relocate the Terminal into distinct storage, then verify the
1890    /// callback still fires through the stable VTable userdata pointer.
1891    #[test]
1892    fn callbacks_survive_explicit_relocation() {
1893        let callback_count = RefCell::new(0usize);
1894        let terminal = build_terminal(&callback_count);
1895        let (mut terminal, addr_before, addr_after) = relocate_into_new_box(terminal);
1896        assert_ne!(addr_before, addr_after);
1897
1898        // Primary DA request (CSI c) should invoke on_device_attributes.
1899        terminal.vt_write(b"\x1b[c");
1900        assert_eq!(*callback_count.borrow(), 1);
1901    }
1902
1903    fn tiny_terminal() -> Terminal<'static, 'static> {
1904        Terminal::new(Options {
1905            cols: 8,
1906            rows: 3,
1907            max_scrollback: 100,
1908        })
1909        .expect("terminal should initialize")
1910    }
1911
1912    fn codepoint_at_tracked_ref(terminal: &Terminal<'_, '_>, tracked: &TrackedGridRef) -> u32 {
1913        let snapshot = tracked
1914            .snapshot(terminal)
1915            .expect("tracked snapshot should not fail")
1916            .expect("tracked ref should have a value");
1917        snapshot
1918            .cell()
1919            .expect("tracked snapshot should resolve to a cell")
1920            .codepoint()
1921            .expect("tracked snapshot cell should expose a codepoint")
1922    }
1923
1924    #[test]
1925    fn tracked_grid_ref_follows_scroll() {
1926        let mut terminal = tiny_terminal();
1927        terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1928
1929        let tracked = terminal
1930            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1931            .expect("tracked grid ref should initialize");
1932
1933        terminal.vt_write(b"\r\ndelta");
1934
1935        assert!(tracked.has_value());
1936        assert_eq!(
1937            codepoint_at_tracked_ref(&terminal, &tracked),
1938            u32::from('a')
1939        );
1940        assert_eq!(
1941            tracked
1942                .point(PointSpace::Screen)
1943                .expect("tracked point should resolve")
1944                .expect("tracked point should have a value")
1945                .x,
1946            0
1947        );
1948    }
1949
1950    #[test]
1951    fn tracked_grid_ref_reports_loss_and_can_set_point() {
1952        let mut terminal = tiny_terminal();
1953        terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1954
1955        let mut tracked = terminal
1956            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1957            .expect("tracked grid ref should initialize");
1958
1959        terminal.reset();
1960
1961        assert!(!tracked.has_value());
1962        assert!(
1963            tracked
1964                .snapshot(&terminal)
1965                .expect("missing tracked snapshot should not fail")
1966                .is_none()
1967        );
1968        assert!(
1969            tracked
1970                .point(PointSpace::Screen)
1971                .expect("missing tracked point should not fail")
1972                .is_none()
1973        );
1974
1975        terminal.vt_write(b"echo");
1976        tracked
1977            .set(&mut terminal, Point::Active(PointCoordinate { x: 0, y: 0 }))
1978            .expect("tracked grid ref should set to a new point");
1979
1980        assert!(tracked.has_value());
1981        assert_eq!(
1982            codepoint_at_tracked_ref(&terminal, &tracked),
1983            u32::from('e')
1984        );
1985    }
1986
1987    #[test]
1988    fn tracked_grid_ref_survives_terminal_drop() {
1989        let tracked = {
1990            let mut terminal = tiny_terminal();
1991            terminal.vt_write(b"alpha");
1992            terminal
1993                .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1994                .expect("tracked grid ref should initialize")
1995        };
1996
1997        assert!(!tracked.has_value());
1998        assert!(
1999            tracked
2000                .point(PointSpace::Screen)
2001                .expect("detached tracked point should not fail")
2002                .is_none()
2003        );
2004    }
2005
2006    #[test]
2007    fn tracked_grid_ref_rejects_different_terminal() {
2008        let mut first = tiny_terminal();
2009        first.vt_write(b"alpha");
2010        let mut second = tiny_terminal();
2011        second.vt_write(b"bravo");
2012
2013        let mut tracked = first
2014            .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
2015            .expect("tracked grid ref should initialize");
2016
2017        assert!(matches!(
2018            tracked.snapshot(&second),
2019            Err(Error::InvalidValue)
2020        ));
2021        assert!(matches!(
2022            tracked.set(&mut second, Point::Active(PointCoordinate { x: 0, y: 0 })),
2023            Err(Error::InvalidValue)
2024        ));
2025    }
2026
2027    #[test]
2028    fn grid_ref_converts_back_to_point() {
2029        let mut terminal = tiny_terminal();
2030        terminal.vt_write(b"alpha");
2031
2032        let original = PointCoordinate { x: 1, y: 0 };
2033        let grid_ref = terminal
2034            .grid_ref(Point::Active(original))
2035            .expect("grid ref should resolve");
2036
2037        assert_eq!(
2038            terminal
2039                .point_from_grid_ref(&grid_ref, PointSpace::Active)
2040                .expect("grid ref point conversion should not fail")
2041                .expect("grid ref should be representable in active space"),
2042            original
2043        );
2044    }
2045}