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}