Skip to main content

libghostty_vt/
terminal.rs

1//! Types and functions around terminal state management.
2
3use std::{marker::PhantomData, mem::MaybeUninit};
4
5use crate::{
6    alloc::{Allocator, Object},
7    error::{Error, Result, from_result},
8    ffi, key,
9    screen::GridRef,
10    style,
11};
12
13#[doc(inline)]
14pub use ffi::GhosttySizeReportSize as SizeReportSize;
15
16/// Complete terminal emulator state and rendering.
17///
18/// A terminal instance manages the full emulator state including the screen,
19/// scrollback, cursor, styles, modes, and VT stream processing.
20///
21/// Once a terminal session is up and running, you can configure a key encoder
22/// to write keyboard input via [`key::Encoder::set_options_from_terminal`].
23///
24/// # Effects
25///
26/// By default, the terminal sequence processing with [`Terminal::vt_write`]
27/// only process sequences that directly affect terminal state and ignores
28/// sequences that have side effect behavior or require responses. These
29/// sequences include things like bell characters, title changes, device
30/// attributes queries, and more. To handle these sequences, the user
31/// must configure "effects."
32///
33/// Effects are callbacks that the terminal invokes in response to VT sequences
34/// processed during [`Terminal::vt_write`]. They let the embedding application
35/// react to terminal-initiated events such as bell characters, title changes,
36/// device status report responses, and more.
37///
38/// Each effect is registered with its corresponding `Terminal::on_<effect>`
39/// function, which accepts a closure with access to the terminal state and
40/// possibly other parameters. Some examples include [`Terminal::on_bell`]
41/// and [`Terminal::on_pty_write`].
42///
43/// All callbacks are invoked synchronously during [`Terminal::vt_write`].
44/// Callbacks must be very careful to not block for too long or perform
45/// expensive operations, since they are blocking further IO processing.
46///
47/// ## Shared state
48///
49/// **Unlike the C API**, you *cannot* specify arbitrary user data that's
50/// shared between all callbacks, mainly because a safe, idiomatic Rust
51/// equivalent of this pattern is very difficult to implement and use
52/// due to Rust's much stricter safety guarantees. In turn, we use the
53/// user data internally for callback dispatch purposes.
54///
55/// You should instead use idiomatic Rust mechanisms like [`Rc`](std::rc::Rc)s
56/// to hold common, mutable state between callbacks (which is perfectly safe,
57/// since everything is run on a single thread within a single `vt_write` call),
58/// or with some other type with interior mutability.
59///
60/// ## Example: Registering effects and processing VT data
61///
62/// ```rust
63/// use std::{cell::Cell, rc::Rc};
64/// use libghostty_vt::{Terminal, TerminalOptions};
65///
66/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
67/// let mut terminal = Terminal::new(TerminalOptions {
68///     cols: 80,
69///     rows: 24,
70///     max_scrollback: 0,
71/// })?;
72///
73/// // Set up a simple bell counter
74/// let bell_count = Rc::new(Cell::new(0usize));
75/// terminal
76///     .on_pty_write(|_term, data| {
77///         println!("Replying {} bytes to the PTY", data.len());
78///     })?
79///    .on_bell({
80///        let bell_count = bell_count.clone();
81///        move |_term| {
82///            bell_count.update(|v| v + 1);
83///            println!("Bell! (count = {})", bell_count.get())
84///        }
85///     })?
86///    .on_title_changed(|term| {
87///        // Query the cursor position to confirm the terminal processed the
88///        // title change (the title itself is tracked by the embedder via the
89///        // OSC parser or its own state).
90///        let col = term.cursor_x().unwrap();
91///        println!("Title changed! (cursor at col {col})");
92///    })?;
93///
94/// // Feed VT data that triggers effects:
95/// // 1. Bell (BEL = 0x07)
96/// terminal.vt_write(b"\x07");
97/// // 2. Title change (OSC 2 ; <title> ST)
98/// terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
99/// // 3. Device status report (DECRQM for wraparound mode ?7)
100/// //    triggers write_pty with the response
101/// terminal.vt_write(b"\x1B[?7$p");
102/// // 4. Another bell to show the counter increments
103/// terminal.vt_write(b"\x07");
104///
105/// assert_eq!(bell_count.get(), 2);
106/// # Ok(())}
107/// ```
108#[derive(Debug)]
109pub struct Terminal<'alloc: 'cb, 'cb> {
110    pub(crate) inner: Object<'alloc, ffi::GhosttyTerminal>,
111    vtable: VTable<'alloc, 'cb>,
112}
113
114/// Terminal initialization options.
115#[derive(Clone, Copy, Debug)]
116pub struct Options {
117    /// Terminal width in cells. Must be greater than zero.
118    pub cols: u16,
119    /// Terminal height in cells. Must be greater than zero.
120    pub rows: u16,
121    /// Maximum number of lines to keep in scrollback history.
122    pub max_scrollback: usize,
123}
124
125impl From<Options> for ffi::GhosttyTerminalOptions {
126    fn from(value: Options) -> Self {
127        Self {
128            cols: value.cols,
129            rows: value.rows,
130            max_scrollback: value.max_scrollback,
131        }
132    }
133}
134
135impl<'alloc: 'cb, 'cb> Terminal<'alloc, 'cb> {
136    /// Create a new terminal instance.
137    pub fn new(opts: Options) -> Result<Self> {
138        // SAFETY: A NULL allocator is always valid
139        unsafe { Self::new_inner(std::ptr::null(), opts) }
140    }
141
142    /// Create a new terminal instance with a custom allocator.
143    ///
144    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
145    /// regarding custom memory management and lifetimes.
146    pub fn new_with_alloc<'ctx: 'alloc, Ctx>(
147        alloc: &'alloc Allocator<'ctx, Ctx>,
148        opts: Options,
149    ) -> Result<Self> {
150        // SAFETY: Borrow checking should forbid invalid allocators
151        unsafe { Self::new_inner(alloc.to_raw(), opts) }
152    }
153
154    unsafe fn new_inner(alloc: *const ffi::GhosttyAllocator, opts: Options) -> Result<Self> {
155        let mut raw: ffi::GhosttyTerminal_ptr = std::ptr::null_mut();
156        let result = unsafe { ffi::ghostty_terminal_new(alloc, &raw mut raw, opts.into()) };
157        from_result(result)?;
158        Ok(Self {
159            inner: Object::new(raw)?,
160            vtable: VTable::default(),
161        })
162    }
163
164    /// Write VT-encoded data to the terminal for processing.
165    ///
166    /// Feeds raw bytes through the terminal's VT stream parser, updating
167    /// terminal state accordingly. By default, sequences that require output
168    /// (queries, device status reports) are silently ignored.
169    /// Use [`Terminal::on_pty_write`] to install a callback that receives
170    /// response data.
171    ///
172    /// This never fails. Any erroneous input or errors in processing the input
173    /// are logged internally but do not cause this function to fail because
174    /// this input is assumed to be untrusted and from an external source; so
175    /// the primary goal is to keep the terminal state consistent and not allow
176    /// malformed input to corrupt or crash.    
177    pub fn vt_write(&mut self, data: &[u8]) {
178        unsafe { ffi::ghostty_terminal_vt_write(self.inner.as_raw(), data.as_ptr(), data.len()) }
179    }
180
181    /// Resize the terminal to the given dimensions.
182    ///
183    /// Changes the number of columns and rows in the terminal. The primary
184    /// screen will reflow content if wraparound mode is enabled; the alternate
185    /// screen does not reflow. If the dimensions are unchanged, this is a no-op.
186    ///
187    /// This also updates the terminal's pixel dimensions (used for image
188    /// protocols and size reports), disables synchronized output mode (allowed
189    /// by the spec so that resize results are shown immediately), and sends an
190    /// in-band size report if mode 2048 is enabled.
191    pub fn resize(
192        &mut self,
193        cols: u16,
194        rows: u16,
195        cell_width_px: u32,
196        cell_height_px: u32,
197    ) -> Result<()> {
198        let result = unsafe {
199            ffi::ghostty_terminal_resize(
200                self.inner.as_raw(),
201                cols,
202                rows,
203                cell_width_px,
204                cell_height_px,
205            )
206        };
207        from_result(result)
208    }
209
210    /// Perform a full reset of the terminal (RIS).
211    ///
212    /// Resets all terminal state back to its initial configuration,
213    /// including modes, scrollback, scrolling region, and screen contents.
214    /// The terminal dimensions are preserved.
215    pub fn reset(&mut self) {
216        unsafe { ffi::ghostty_terminal_reset(self.inner.as_raw()) }
217    }
218
219    /// Scroll the terminal viewport.
220    pub fn scroll_viewport(&mut self, scroll: ScrollViewport) {
221        unsafe { ffi::ghostty_terminal_scroll_viewport(self.inner.as_raw(), scroll.into()) }
222    }
223
224    /// Resolve a point in the terminal grid to a grid reference.
225    ///
226    /// Resolves the given point (which can be in active, viewport, screen,
227    /// or history coordinates) to a grid reference for that location. Use
228    /// [`GridRef::cell`] and [`GridRef::row`] to extract the cell and row.
229    ///
230    /// Lookups in the active region and viewport are fast. Lookups in the
231    /// screen and history may require traversing the full scrollback page
232    /// list to resolve the y coordinate, so they can be expensive for large
233    /// scrollback buffers.
234    ///
235    /// This function isn't meant to be used as the core of render loop. It
236    /// isn't built to sustain the framerates needed for rendering large
237    /// screens. Use the [render state API](crate::render::RenderState) for
238    /// that. This API is instead meant for less strictly performance-sensitive
239    /// use cases.
240    pub fn grid_ref(&self, point: Point) -> Result<GridRef<'_>> {
241        let mut grid_ref = ffi::sized!(ffi::GhosttyGridRef);
242        let result = unsafe {
243            ffi::ghostty_terminal_grid_ref(self.inner.as_raw(), point.into(), &raw mut grid_ref)
244        };
245        from_result(result)?;
246        Ok(GridRef {
247            inner: grid_ref,
248            _phan: PhantomData,
249        })
250    }
251
252    /// Get the current value of a terminal mode.
253    pub fn mode(&self, mode: Mode) -> Result<bool> {
254        let mut value = false;
255        let result = unsafe {
256            ffi::ghostty_terminal_mode_get(self.inner.as_raw(), mode.into(), &raw mut value)
257        };
258        from_result(result)?;
259        Ok(value)
260    }
261
262    /// Set the value of a terminal mode.
263    pub fn set_mode(&mut self, mode: Mode, value: bool) -> Result<()> {
264        let result =
265            unsafe { ffi::ghostty_terminal_mode_set(self.inner.as_raw(), mode.into(), value) };
266        from_result(result)
267    }
268
269    fn get<T>(&self, tag: ffi::GhosttyTerminalData) -> Result<T> {
270        let mut value = MaybeUninit::<T>::zeroed();
271        let result = unsafe {
272            ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
273        };
274        from_result(result)?;
275        // SAFETY: Value should be initialized after successful call.
276        Ok(unsafe { value.assume_init() })
277    }
278
279    fn set<T>(&self, tag: ffi::GhosttyTerminalOption, v: &T) -> Result<()> {
280        let result = unsafe {
281            ffi::ghostty_terminal_set(self.inner.as_raw(), tag, std::ptr::from_ref(v).cast())
282        };
283        from_result(result)
284    }
285
286    /// Get the terminal width in cells.
287    pub fn cols(&self) -> Result<u16> {
288        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_COLS)
289    }
290    /// Get the terminal height in cells.
291    pub fn rows(&self) -> Result<u16> {
292        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ROWS)
293    }
294    /// Get the cursor column position (inner-indexed).
295    pub fn cursor_x(&self) -> Result<u16> {
296        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_CURSOR_X)
297    }
298    /// Get the cursor row position within the active area (inner-indexed).
299    pub fn cursor_y(&self) -> Result<u16> {
300        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_CURSOR_Y)
301    }
302    /// Get whether the cursor has a pending wrap (next print will soft-wrap).
303    pub fn is_cursor_pending_wrap(&self) -> Result<bool> {
304        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP)
305    }
306    /// Get whether the cursor is visible (DEC mode 25).
307    pub fn is_cursor_visible(&self) -> Result<bool> {
308        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE)
309    }
310    /// Get the current SGR style of the cursor.
311    ///
312    /// This is the style that will be applied to newly printed characters.
313    pub fn cursor_style(&self) -> Result<style::Style> {
314        self.get::<ffi::GhosttyStyle>(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_CURSOR_STYLE)
315            .and_then(std::convert::TryInto::try_into)
316    }
317    /// Get the current Kitty keyboard protocol flags.
318    pub fn kitty_keyboard_flags(&self) -> Result<key::KittyKeyFlags> {
319        self.get::<ffi::GhosttyKittyKeyFlags>(
320            ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS,
321        )
322        .map(key::KittyKeyFlags::from_bits_retain)
323    }
324
325    /// Get the scrollbar state for the terminal viewport.
326    ///
327    /// This may be expensive to calculate depending on where the viewport is
328    /// (arbitrary pins are expensive). The caller should take care to only call
329    /// this as needed and not too frequently.
330    pub fn scrollbar(&self) -> Result<ffi::GhosttyTerminalScrollbar> {
331        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_SCROLLBAR)
332    }
333    /// Get the currently active screen.
334    pub fn active_screen(&self) -> Result<ffi::GhosttyTerminalScreen> {
335        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN)
336    }
337    /// Get whether any mouse tracking mode is active.
338    ///
339    /// Returns true if any of the mouse tracking modes (X1inner, normal, button,
340    /// or any-event) are enabled.
341    pub fn is_mouse_tracking(&self) -> Result<bool> {
342        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING)
343    }
344    /// Get the terminal title as set by escape sequences (e.g. OSC inner/2).
345    ///
346    /// Returns a borrowed string, valid until the next call to
347    /// [`Terminal::vt_write`] or [`Terminal::reset`]. An empty string is
348    /// returned when no title has been set.
349    pub fn title(&self) -> Result<&str> {
350        let str =
351            self.get::<ffi::GhosttyString>(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_TITLE)?;
352        // SAFETY: We trust libghostty to return a valid borrowed string,
353        // while we uphold that no mutation could happen during its lifetime.
354        let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
355        std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
356    }
357
358    /// Get the current working directory as set by escape sequences (e.g. OSC 7).
359    ///
360    /// Returns a borrowed string, valid until the next call to
361    /// [`Terminal::vt_write`] or [`Terminal::reset`]. An empty string is
362    /// returned when no title has been set.
363    pub fn pwd(&self) -> Result<&str> {
364        let str =
365            self.get::<ffi::GhosttyString>(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_PWD)?;
366        // SAFETY: We trust libghostty to return a valid borrowed string,
367        // while we uphold that no mutation could happen during its lifetime.
368        let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
369        std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
370    }
371    /// The total number of rows in the active screen including scrollback.
372    pub fn total_rows(&self) -> Result<usize> {
373        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_TOTAL_ROWS)
374    }
375    ///  The number of scrollback rows (total rows minus viewport rows).
376    pub fn scrollback_rows(&self) -> Result<usize> {
377        self.get(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS)
378    }
379}
380
381impl Drop for Terminal<'_, '_> {
382    fn drop(&mut self) {
383        unsafe { ffi::ghostty_terminal_free(self.inner.as_raw()) }
384    }
385}
386
387/// A point in the terminal grid.
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389pub enum Point {
390    /// Active area where the cursor can move.
391    Active(PointCoordinate),
392    /// Visible viewport (changes when scrolled).
393    Viewport(PointCoordinate),
394    /// Full screen including scrollback.
395    Screen(PointCoordinate),
396    /// Scrollback history only (before active area).
397    History(PointCoordinate),
398}
399
400impl From<Point> for ffi::GhosttyPoint {
401    fn from(value: Point) -> Self {
402        match value {
403            Point::Active(coord) => Self {
404                tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_ACTIVE,
405                value: ffi::GhosttyPointValue {
406                    coordinate: coord.into(),
407                },
408            },
409            Point::Viewport(coord) => Self {
410                tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_VIEWPORT,
411                value: ffi::GhosttyPointValue {
412                    coordinate: coord.into(),
413                },
414            },
415            Point::Screen(coord) => Self {
416                tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN,
417                value: ffi::GhosttyPointValue {
418                    coordinate: coord.into(),
419                },
420            },
421            Point::History(coord) => Self {
422                tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_HISTORY,
423                value: ffi::GhosttyPointValue {
424                    coordinate: coord.into(),
425                },
426            },
427        }
428    }
429}
430
431/// A coordinate in the terminal grid.
432#[derive(Clone, Copy, Debug, PartialEq, Eq)]
433pub struct PointCoordinate {
434    /// Column (0-indexed).
435    x: u16,
436    /// Row (0-indexed). May exceed page size for screen/history tags.
437    y: u32,
438}
439impl From<PointCoordinate> for ffi::GhosttyPointCoordinate {
440    fn from(value: PointCoordinate) -> Self {
441        let PointCoordinate { x, y } = value;
442        Self { x, y }
443    }
444}
445impl From<ffi::GhosttyPointCoordinate> for PointCoordinate {
446    fn from(value: ffi::GhosttyPointCoordinate) -> Self {
447        let ffi::GhosttyPointCoordinate { x, y } = value;
448        Self { x, y }
449    }
450}
451
452/// Scroll viewport behavior.
453#[derive(Clone, Copy, Debug, PartialEq, Eq)]
454pub enum ScrollViewport {
455    /// Scroll to the top of the scrollback.
456    Top,
457    /// Scroll to the bottom (active area).
458    Bottom,
459    /// Scroll by a delta amount (up is negative).
460    Delta(isize),
461}
462impl From<ScrollViewport> for ffi::GhosttyTerminalScrollViewport {
463    fn from(value: ScrollViewport) -> Self {
464        match value {
465            ScrollViewport::Top => Self {
466                tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_TOP,
467                value: ffi::GhosttyTerminalScrollViewportValue::default(),
468            },
469            ScrollViewport::Bottom => Self {
470                tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_BOTTOM,
471                value: ffi::GhosttyTerminalScrollViewportValue::default(),
472            },
473            ScrollViewport::Delta(delta) => Self {
474                tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_DELTA,
475                value: {
476                    let mut v = ffi::GhosttyTerminalScrollViewportValue::default();
477                    v.delta = delta;
478                    v
479                },
480            },
481        }
482    }
483}
484
485/// A terminal mode consisting of its value and its kind (DEC/ANSI).
486#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
487pub struct Mode(pub ffi::GhosttyMode);
488
489impl Mode {
490    #![expect(missing_docs, reason = "no upstream documentation provided")]
491    const ANSI_BIT: u16 = 1 << 15;
492
493    /// Create a new mode from its numeric value and its kind.
494    #[must_use]
495    pub const fn new(v: u16, kind: ModeKind) -> Self {
496        match kind {
497            ModeKind::Ansi => Self(v | Self::ANSI_BIT),
498            ModeKind::Dec => Self(v),
499        }
500    }
501
502    /// The numeric value of the mode.
503    #[must_use]
504    pub fn value(self) -> u16 {
505        (self.0) & 0x7fff
506    }
507
508    /// The kind of the mode (DEC/ANSI).
509    #[must_use]
510    pub fn kind(self) -> ModeKind {
511        if (self.0) & Self::ANSI_BIT > 0 {
512            ModeKind::Ansi
513        } else {
514            ModeKind::Dec
515        }
516    }
517
518    pub const KAM: Self = Self::new(2, ModeKind::Ansi);
519    pub const INSERT: Self = Self::new(4, ModeKind::Ansi);
520    pub const SRM: Self = Self::new(12, ModeKind::Ansi);
521    pub const LINEFEED: Self = Self::new(20, ModeKind::Ansi);
522
523    pub const DECCKM: Self = Self::new(1, ModeKind::Dec);
524    pub const _132_COLUMN: Self = Self::new(3, ModeKind::Dec);
525    pub const SLOW_SCROLL: Self = Self::new(4, ModeKind::Dec);
526    pub const REVERSE_COLORS: Self = Self::new(5, ModeKind::Dec);
527    pub const ORIGIN: Self = Self::new(6, ModeKind::Dec);
528    pub const WRAPAROUND: Self = Self::new(7, ModeKind::Dec);
529    pub const AUTOREPEAT: Self = Self::new(8, ModeKind::Dec);
530    pub const X10_MOUSE: Self = Self::new(9, ModeKind::Dec);
531    pub const CURSOR_BLINKING: Self = Self::new(12, ModeKind::Dec);
532    pub const CURSOR_VISIBLE: Self = Self::new(25, ModeKind::Dec);
533    pub const ENABLE_MODE3: Self = Self::new(40, ModeKind::Dec);
534    pub const REVERSE_WRAP: Self = Self::new(45, ModeKind::Dec);
535    pub const ALT_SCREEN_LEGACY: Self = Self::new(47, ModeKind::Dec);
536    pub const KEYPAD_KEYS: Self = Self::new(66, ModeKind::Dec);
537    pub const LEFT_RIGHT_MARGIN: Self = Self::new(69, ModeKind::Dec);
538    pub const NORMAL_MOUSE: Self = Self::new(1000, ModeKind::Dec);
539    pub const BUTTON_MOUSE: Self = Self::new(1002, ModeKind::Dec);
540    pub const ANY_MOUSE: Self = Self::new(1003, ModeKind::Dec);
541    pub const FOCUS_EVENT: Self = Self::new(1004, ModeKind::Dec);
542    pub const UTF8_MOUSE: Self = Self::new(1005, ModeKind::Dec);
543    pub const SGR_MOUSE: Self = Self::new(1006, ModeKind::Dec);
544    pub const ALT_SCROLL: Self = Self::new(1007, ModeKind::Dec);
545    pub const URXVT_MOUSE: Self = Self::new(1015, ModeKind::Dec);
546    pub const SGR_PIXELS_MOUSE: Self = Self::new(1016, ModeKind::Dec);
547    pub const NUMLOCK_KEYPAD: Self = Self::new(1035, ModeKind::Dec);
548    pub const ALT_ESC_PREFIX: Self = Self::new(1036, ModeKind::Dec);
549    pub const ALT_SENDS_ESC: Self = Self::new(1039, ModeKind::Dec);
550    pub const REVERSE_WRAP_EXT: Self = Self::new(1045, ModeKind::Dec);
551    pub const ALT_SCREEN: Self = Self::new(1047, ModeKind::Dec);
552    pub const SAVE_CURSOR: Self = Self::new(1048, ModeKind::Dec);
553    pub const ALT_SCREEN_SAVE: Self = Self::new(1049, ModeKind::Dec);
554    pub const BRACKETED_PASTE: Self = Self::new(2004, ModeKind::Dec);
555    pub const SYNC_OUTPUT: Self = Self::new(2026, ModeKind::Dec);
556    pub const GRAPHEME_CLUSTER: Self = Self::new(2027, ModeKind::Dec);
557    pub const COLOR_SCHEME_REPORT: Self = Self::new(2031, ModeKind::Dec);
558    pub const IN_BAND_RESIZE: Self = Self::new(2048, ModeKind::Dec);
559}
560
561/// The kind of a terminal mode.
562#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
563pub enum ModeKind {
564    /// DEC terminal mode.
565    Dec,
566    /// ANSI terminal mode.
567    Ansi,
568}
569
570impl From<Mode> for ffi::GhosttyMode {
571    fn from(value: Mode) -> Self {
572        value.0
573    }
574}
575
576/// Device attributes response data for all three DA levels.
577/// Filled by the [`Terminal::on_device_attributes`] callback in response
578/// to CSI c, CSI > c, or CSI = c queries. The terminal uses whichever
579/// sub-struct matches the request type.
580#[derive(Debug, Clone, Copy)]
581pub struct DeviceAttributes {
582    /// Primary device attributes (DA1).
583    pub primary: PrimaryDeviceAttributes,
584    /// Secondary device attributes (DA2).
585    pub secondary: SecondaryDeviceAttributes,
586    /// Tertiary device attributes (DA3).
587    pub tertiary: TertiaryDeviceAttributes,
588}
589
590impl From<DeviceAttributes> for ffi::GhosttyDeviceAttributes {
591    fn from(value: DeviceAttributes) -> Self {
592        Self {
593            primary: value.primary.into(),
594            secondary: value.secondary.into(),
595            tertiary: value.tertiary.into(),
596        }
597    }
598}
599
600/// Primary device attributes (DA1) response data.
601///
602/// Returned as part of [`DeviceAttributes`] in response to a CSI c query.
603#[derive(Debug, Clone, Copy)]
604pub struct PrimaryDeviceAttributes(ffi::GhosttyDeviceAttributesPrimary);
605
606impl PrimaryDeviceAttributes {
607    /// Construct primary device attributes from a conformance level
608    /// and an array of device attribute features.
609    ///
610    /// # Panics
611    ///
612    /// **Panics** when more than 64 features are given.
613    #[must_use]
614    pub fn new<const N: usize>(
615        conformance_level: ConformanceLevel,
616        features: [DeviceAttributeFeature; N],
617    ) -> Self {
618        assert!(N <= 64);
619
620        let mut f = [0u16; 64];
621        f[..N].copy_from_slice(features.map(|f| f.0).as_slice());
622
623        Self(ffi::GhosttyDeviceAttributesPrimary {
624            conformance_level: conformance_level.0,
625            features: f,
626            num_features: N,
627        })
628    }
629}
630
631impl From<PrimaryDeviceAttributes> for ffi::GhosttyDeviceAttributesPrimary {
632    fn from(value: PrimaryDeviceAttributes) -> Self {
633        value.0
634    }
635}
636
637/// The level of conformance to the behavior of a specific or a family of
638/// physical terminal models.
639#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
640pub struct ConformanceLevel(pub u16);
641
642impl ConformanceLevel {
643    #![allow(clippy::cast_possible_truncation, reason = "bindgen ain't perfect")]
644    #![expect(missing_docs, reason = "self-explanatory")]
645    pub const VT100: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT100 as u16);
646    pub const VT101: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT101 as u16);
647    pub const VT102: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT102 as u16);
648    pub const VT125: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT125 as u16);
649    pub const VT131: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT131 as u16);
650    pub const VT132: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT132 as u16);
651    pub const VT220: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT220 as u16);
652    pub const VT240: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT240 as u16);
653    pub const VT320: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT320 as u16);
654    pub const VT340: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT340 as u16);
655    pub const VT420: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT420 as u16);
656    pub const VT510: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT510 as u16);
657    pub const VT520: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT520 as u16);
658    pub const VT525: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_VT525 as u16);
659    /// Equivalent to a VT2xx terminal.
660    pub const LEVEL_2: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_LEVEL_2 as u16);
661    /// Equivalent to a VT3xx terminal.
662    pub const LEVEL_3: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_LEVEL_3 as u16);
663    /// Equivalent to a VT4xx terminal.
664    pub const LEVEL_4: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_LEVEL_4 as u16);
665    /// Equivalent to a VT5xx terminal.
666    pub const LEVEL_5: Self = Self(ffi::GHOSTTY_DA_CONFORMANCE_LEVEL_5 as u16);
667}
668
669/// A feature that a terminal can report to support.
670#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
671pub struct DeviceAttributeFeature(pub u16);
672
673impl DeviceAttributeFeature {
674    #![expect(clippy::cast_possible_truncation, reason = "bindgen ain't perfect")]
675    #![expect(missing_docs, reason = "no upstream documentation provided")]
676    pub const COLUMNS_132: Self = Self(ffi::GHOSTTY_DA_FEATURE_COLUMNS_132 as u16);
677    pub const PRINTER: Self = Self(ffi::GHOSTTY_DA_FEATURE_PRINTER as u16);
678    pub const REGIS: Self = Self(ffi::GHOSTTY_DA_FEATURE_REGIS as u16);
679    pub const SIXEL: Self = Self(ffi::GHOSTTY_DA_FEATURE_SIXEL as u16);
680    pub const SELECTIVE_ERASE: Self = Self(ffi::GHOSTTY_DA_FEATURE_SELECTIVE_ERASE as u16);
681    pub const USER_DEFINED_KEYS: Self = Self(ffi::GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS as u16);
682    pub const NATIONAL_REPLACEMENT: Self =
683        Self(ffi::GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT as u16);
684    pub const TECHNICAL_CHARACTERS: Self =
685        Self(ffi::GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS as u16);
686    pub const LOCATOR: Self = Self(ffi::GHOSTTY_DA_FEATURE_LOCATOR as u16);
687    pub const TERMINAL_STATE: Self = Self(ffi::GHOSTTY_DA_FEATURE_TERMINAL_STATE as u16);
688    pub const WINDOWING: Self = Self(ffi::GHOSTTY_DA_FEATURE_WINDOWING as u16);
689    pub const HORIZONTAL_SCROLLING: Self =
690        Self(ffi::GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING as u16);
691    pub const ANSI_COLOR: Self = Self(ffi::GHOSTTY_DA_FEATURE_ANSI_COLOR as u16);
692    pub const RECTANGULAR_EDITING: Self = Self(ffi::GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING as u16);
693    pub const ANSI_TEXT_LOCATOR: Self = Self(ffi::GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR as u16);
694    pub const CLIPBOARD: Self = Self(ffi::GHOSTTY_DA_FEATURE_CLIPBOARD as u16);
695}
696
697/// Secondary device attributes (DA2) response data.
698///
699/// Returned as part of [`DeviceAttributes`] in response to a CSI > c query.
700/// Response format: CSI > Pp ; Pv ; Pc c
701#[derive(Debug, Copy, Clone)]
702pub struct SecondaryDeviceAttributes {
703    /// Terminal type identifier (Pp).
704    pub device_type: DeviceType,
705    /// Firmware/patch version number (Pv).
706    pub firmware_version: u16,
707    /// ROM cartridge registration number (Pc). Always 0 for emulators.
708    pub rom_cartridge: u16,
709}
710
711impl From<SecondaryDeviceAttributes> for ffi::GhosttyDeviceAttributesSecondary {
712    fn from(value: SecondaryDeviceAttributes) -> Self {
713        Self {
714            device_type: value.device_type.0,
715            firmware_version: value.firmware_version,
716            rom_cartridge: value.rom_cartridge,
717        }
718    }
719}
720
721/// The type of terminal device being emulated.
722#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
723pub struct DeviceType(pub u16);
724
725impl DeviceType {
726    #![expect(clippy::cast_possible_truncation, reason = "bindgen ain't perfect")]
727    #![expect(missing_docs, reason = "self-explanatory")]
728    pub const VT100: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT100 as u16);
729    pub const VT220: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT220 as u16);
730    pub const VT240: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT240 as u16);
731    pub const VT330: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT330 as u16);
732    pub const VT340: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT340 as u16);
733    pub const VT320: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT320 as u16);
734    pub const VT382: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT382 as u16);
735    pub const VT420: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT420 as u16);
736    pub const VT510: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT510 as u16);
737    pub const VT520: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT520 as u16);
738    pub const VT525: Self = Self(ffi::GHOSTTY_DA_DEVICE_TYPE_VT525 as u16);
739}
740
741/// Tertiary device attributes (DA3) response data.
742///
743/// Returned as part of [`DeviceAttributes`] in response to a CSI = c query.
744/// Response format: DCS ! | D...D ST (DECRPTUI).
745#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
746pub struct TertiaryDeviceAttributes {
747    /// Unit ID encoded as 8 uppercase hex digits in the response.
748    pub unit_id: u32,
749}
750
751impl From<TertiaryDeviceAttributes> for ffi::GhosttyDeviceAttributesTertiary {
752    fn from(value: TertiaryDeviceAttributes) -> Self {
753        Self {
754            unit_id: value.unit_id,
755        }
756    }
757}
758
759//---------------------------------------
760// Callbacks
761//---------------------------------------
762
763/// You might be wondering just what the heck this is.
764///
765/// Truth to be told, you don't need to understand how it works
766/// in order to use it. It does a bunch of voodoo behind the scenes
767/// that make sure all the invariants of the C API are upheld, while
768/// providing a convenient API for Rust users.
769///
770/// Each handler is defined in this following format:
771/// ```ignore
772/// pub fn on_foobar(
773///     &mut self,
774///     // The corresponding GhosttyTerminalOption
775///     tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_FOOBAR,
776///
777///     // The name of the original function type in C,
778///     // along with the extra C parameters and the expected C return type
779///     from = GhosttyTerminalFoobarFn(foo: *const u8, bar: usize) -> bool,
780///
781///     // The name of mapped Rust function type,
782///     // along with the Rust parameters and return type.
783///     //
784///     // `<'t>` is used to tie the return value to the lifetime of the
785///     // terminal. The name is arbitrary - any lifetime marker will do.
786///     to = <'t>FoobarFn(&'t [u8]) -> bool,
787/// ) |term, func| {
788///     // `term` is the terminal and `func` is the Rust callback.
789///     // Both names are arbitrary.
790///
791///     // Convert the raw parameters into Rust types.
792///     // This is just to illustrate how.
793///     let slice = unsafe { std::slice::from_raw_parts(foo, bar) };
794///
795///     // Call into user logic and return.
796///     func(&terminal, slice)
797/// }
798/// ```
799macro_rules! handlers {
800    {
801        $(
802            $(#[$fmeta:meta])*
803            $vis:vis fn $name:ident(
804                &mut self,
805                tag = $tag:ident,
806                from = $rawfnty:ident( $($rfname:ident: $rfty:ty),*$(,)? ) $(-> $rawrty:ty)?,
807                $(#[$tmeta:meta])*
808                to = $(<$lf:lifetime>)? $fnty:ident( $($fty:ty),*$(,)? ) $(-> $rty:ty)?,
809            ) |$t:ident, $func:ident| $block:block
810        )*
811    } => {
812        impl<'alloc, 'cb> $crate::terminal::Terminal<'alloc, 'cb> {$(
813            $(#[$fmeta])*
814            $vis fn $name(&mut self, f: impl $fnty<'alloc, 'cb>) -> $crate::error::Result<&mut Self> {
815                unsafe extern "C" fn callback(
816                    t: *mut $crate::ffi::GhosttyTerminal,
817                    ud: *mut std::ffi::c_void,
818                    $($rfname: $rfty),*
819                ) $(-> $rawrty)? {
820                    // SAFETY: We own the vtable, so it should never become invalid.
821                    let vtable = unsafe { &mut *ud.cast::<VTable<'_, '_>>() };
822
823                    let obj = $crate::alloc::Object::new(t).expect("received null terminal ptr in callback - this is a bug!");
824                    let $t = $crate::terminal::Terminal::<'_, '_> {
825                        inner: obj,
826                        vtable: ::core::default::Default::default(),
827                    };
828                    let $func = vtable.$name.as_deref_mut()
829                        .expect("no handler set but callback is still called - this is a bug!");
830                    let ret = $block;
831
832                    // IMPORTANT: Do NOT let the destructor run.
833                    ::core::mem::forget($t);
834                    ret
835                }
836
837                self.vtable.$name = Some(::std::boxed::Box::new(f));
838
839                self.set(
840                    $crate::ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_USERDATA,
841                    &self.vtable
842                )?;
843
844                // The callback must be coerced into a function *pointer*
845                // and not a function *item* (which is a ZST whose address is meaningless).
846                // :)
847                let callback_ptr: unsafe extern "C" fn(
848                    *mut $crate::ffi::GhosttyTerminal,
849                    *mut ::std::ffi::c_void,
850                    $($rfty),*
851                ) $(-> $rawrty)? = callback;
852
853                let result = unsafe {
854                    $crate::ffi::ghostty_terminal_set(
855                        self.inner.as_raw(),
856                        $crate::ffi::$tag,
857                        callback_ptr as *const ::std::ffi::c_void
858                    )
859                };
860                $crate::error::from_result(result)?;
861                Ok(self)
862            }
863        )*}
864        $(
865            #[doc = concat!(
866                "Callback type for [`Terminal::",
867                stringify!($name),
868                "`](Terminal::",
869                stringify!($name),
870                ").\n"
871            )]
872            $(#[$tmeta])*
873            pub trait $fnty<'alloc, 'cb>:
874                $(for<$lf>)? FnMut(
875                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
876                    $($fty),*
877                ) $(-> $rty)? + 'cb {}
878
879            impl<'alloc, 'cb, F> $fnty<'alloc, 'cb> for F
880            where
881                F: $(for<$lf>)? FnMut(
882                    &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
883                    $($fty),*
884                ) $(-> $rty)? + 'cb
885            {}
886        )*
887
888        struct VTable<'alloc, 'cb> {
889            $($name: Option<::std::boxed::Box<dyn $fnty<'alloc, 'cb>>>),*
890        }
891
892        impl ::core::fmt::Debug for VTable<'_, '_> {
893            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
894                f.write_str("VTable {..}")
895            }
896        }
897
898        impl ::core::default::Default for VTable<'_, '_> {
899            fn default() -> Self {
900                Self {
901                    $($name: None),*
902                }
903            }
904        }
905    };
906}
907
908handlers! {
909    /// Call the given function when the terminal needs to write data back
910    /// to the pty (e.g. in response to a DECRQM query or device status report).
911    pub fn on_pty_write(
912        &mut self,
913        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_WRITE_PTY,
914        from = GhosttyTerminalWritePtyFn(ptr: *const u8, len: usize),
915        to = <'t>PtyWriteFn(&'t [u8]),
916    ) |term, func| {
917        // SAFETY: We trust libghostty to return valid memory given we
918        // uphold all lifetime invariants (e.g. no `vt_write` calls
919        // during this callback, which is guaranteed via the mutable reference).
920        let data = unsafe { std::slice::from_raw_parts(ptr, len) };
921        func(&term, data);
922    }
923
924    /// Call the given function when the terminal receives
925    /// a BEL character (0x07).
926    pub fn on_bell(
927        &mut self,
928        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_BELL,
929        from = GhosttyTerminalBellFn(),
930        to = BellFn(),
931    ) |term, func| {
932        func(&term);
933    }
934
935    /// Call the given function when the terminal receives
936    /// an ENQ character (0x05).
937    pub fn on_enquiry(
938        &mut self,
939        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_ENQUIRY,
940        from = GhosttyTerminalEnquiryFn() -> ffi::GhosttyString,
941        to = <'t>EnquiryFn() -> Option<&'t str>,
942    ) |term, func| {
943        func(&term).unwrap_or("").into()
944    }
945
946    /// Call the given function when the terminal receives an XTVERSION
947    /// query (CSI > q), and respond with the resulting version string
948    /// (e.g. "myterm 1.0").
949    pub fn on_xtversion(
950        &mut self,
951        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_XTVERSION,
952        from = GhosttyTerminalXtversionFn() -> ffi::GhosttyString,
953        to = <'t>XtversionFn() -> Option<&'t str>,
954    ) |term, func| {
955        func(&term).unwrap_or("").into()
956    }
957
958    /// Call the given function when the terminal title changes
959    /// via escape sequences (e.g. OSC 0 or OSC 2).
960    ///
961    /// The new title can be queried from the terminal after
962    /// the callback returns.
963    pub fn on_title_changed(
964        &mut self,
965        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_TITLE_CHANGED,
966        from = GhosttyTerminalTitleChangedFn(),
967        to = TitleChanged(),
968    ) |term, func| {
969        func(&term);
970    }
971
972    /// Call the given function in response to XTWINOPS size queries
973    /// (CSI 14/16/18 t).
974    pub fn on_size(
975        &mut self,
976        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_SIZE,
977        from = GhosttyTerminalSizeFn(out: *mut ffi::GhosttySizeReportSize) -> bool,
978        to = SizeFn() -> Option<ffi::GhosttySizeReportSize>,
979    ) |term, func| {
980        if let Some(size) = func(&term) {
981            // SAFETY: Out pointer is assumed to be valid.
982            unsafe { *out = size };
983            true
984        } else {
985            false
986        }
987    }
988
989    /// Call the given function in response to a color scheme
990    /// device status report query (CSI ? 996 n).
991    ///
992    /// Return `Some` to report the current color scheme,
993    /// or return `None` to silently ignore.
994    pub fn on_color_scheme(
995        &mut self,
996        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_SCHEME,
997        from = GhosttyTerminalColorSchemeFn(out: *mut ffi::GhosttyColorScheme) -> bool,
998        to = ColorSchemeFn() -> Option<ffi::GhosttyColorScheme>,
999    ) |term, func| {
1000        if let Some(size) = func(&term) {
1001            // SAFETY: Out pointer is assumed to be valid.
1002            unsafe { *out = size };
1003            true
1004        } else {
1005            false
1006        }
1007    }
1008
1009    /// Call the given function in response to a device attributes query
1010    /// (CSI c, CSI > c, or CSI = c).
1011    ///
1012    /// Return `Some` with the response data,
1013    /// or return `None` to silently ignore.
1014    pub fn on_device_attributes(
1015        &mut self,
1016        tag = GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES,
1017        from = GhosttyTerminalDeviceAttributesFn(out: *mut ffi::GhosttyDeviceAttributes) -> bool,
1018        to = DeviceAttributesFn() -> Option<DeviceAttributes>,
1019    ) |term, func| {
1020        if let Some(size) = func(&term) {
1021            // SAFETY: Out pointer is assumed to be valid.
1022            unsafe { *out = size.into() };
1023            true
1024        } else {
1025            false
1026        }
1027    }
1028}