use std::{mem::MaybeUninit, ptr::NonNull};
use crate::{
alloc::{Allocator, Object},
error::{Error, Result, from_optional_result_uninit, from_result},
ffi::{self, TerminalData as Data, TerminalOption as Opt},
key,
screen::{GridRef, Screen, TrackedGridRef},
style::{self, RgbColor},
};
#[doc(inline)]
pub use ffi::{SizeReportSize, TerminalScrollbar as Scrollbar};
#[derive(Debug)]
pub struct Terminal<'alloc: 'cb, 'cb> {
pub(crate) inner: Object<'alloc, ffi::TerminalImpl>,
vtable: Box<VTable<'alloc, 'cb>>,
}
#[derive(Clone, Copy, Debug)]
pub struct Options {
pub cols: u16,
pub rows: u16,
pub max_scrollback: usize,
}
impl From<Options> for ffi::TerminalOptions {
fn from(value: Options) -> Self {
Self {
cols: value.cols,
rows: value.rows,
max_scrollback: value.max_scrollback,
}
}
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
#[non_exhaustive]
pub enum CursorStyle {
Bar = ffi::TerminalCursorStyle::BAR,
Block = ffi::TerminalCursorStyle::BLOCK,
Underline = ffi::TerminalCursorStyle::UNDERLINE,
BlockHollow = ffi::TerminalCursorStyle::BLOCK_HOLLOW,
}
impl<'alloc: 'cb, 'cb> Terminal<'alloc, 'cb> {
pub fn new(opts: Options) -> Result<Self> {
unsafe { Self::new_inner(std::ptr::null(), opts) }
}
pub fn new_with_alloc<'ctx: 'alloc>(
alloc: &'alloc Allocator<'ctx>,
opts: Options,
) -> Result<Self> {
unsafe { Self::new_inner(alloc.to_raw(), opts) }
}
unsafe fn new_inner(alloc: *const ffi::Allocator, opts: Options) -> Result<Self> {
let mut raw: ffi::Terminal = std::ptr::null_mut();
let result = unsafe { ffi::ghostty_terminal_new(alloc, &raw mut raw, opts.into()) };
from_result(result)?;
Ok(Self {
inner: Object::new(raw)?,
vtable: Box::new(VTable::default()),
})
}
pub fn vt_write(&mut self, data: &[u8]) {
unsafe { ffi::ghostty_terminal_vt_write(self.inner.as_raw(), data.as_ptr(), data.len()) }
}
pub fn resize(
&mut self,
cols: u16,
rows: u16,
cell_width_px: u32,
cell_height_px: u32,
) -> Result<()> {
let result = unsafe {
ffi::ghostty_terminal_resize(
self.inner.as_raw(),
cols,
rows,
cell_width_px,
cell_height_px,
)
};
from_result(result)
}
pub fn reset(&mut self) {
unsafe { ffi::ghostty_terminal_reset(self.inner.as_raw()) }
}
pub fn scroll_viewport(&mut self, scroll: ScrollViewport) {
unsafe { ffi::ghostty_terminal_scroll_viewport(self.inner.as_raw(), scroll.into()) }
}
pub fn grid_ref(&self, point: Point) -> Result<GridRef<'_>> {
let mut grid_ref = ffi::sized!(ffi::GridRef);
let result = unsafe {
ffi::ghostty_terminal_grid_ref(self.inner.as_raw(), point.into(), &raw mut grid_ref)
};
from_result(result)?;
Ok(unsafe { GridRef::from_raw(grid_ref) })
}
pub fn track_grid_ref(&self, point: Point) -> Result<TrackedGridRef> {
let mut raw: ffi::TrackedGridRef = std::ptr::null_mut();
let result = unsafe {
ffi::ghostty_terminal_grid_ref_track(self.inner.as_raw(), point.into(), &raw mut raw)
};
from_result(result)?;
let inner = NonNull::new(raw).ok_or(Error::InvalidValue)?;
Ok(TrackedGridRef::new(inner, self.inner.ptr))
}
pub fn point_from_grid_ref(
&self,
grid_ref: &GridRef<'_>,
space: PointSpace,
) -> Result<Option<PointCoordinate>> {
let mut point = MaybeUninit::<ffi::PointCoordinate>::zeroed();
let result = unsafe {
ffi::ghostty_terminal_point_from_grid_ref(
self.inner.as_raw(),
std::ptr::from_ref(&grid_ref.inner),
space.into_raw(),
point.as_mut_ptr(),
)
};
from_optional_result_uninit(result, point).map(|value| value.map(Into::into))
}
pub fn mode(&self, mode: Mode) -> Result<bool> {
let mut value = false;
let result = unsafe {
ffi::ghostty_terminal_mode_get(self.inner.as_raw(), mode.into(), &raw mut value)
};
from_result(result)?;
Ok(value)
}
pub fn set_mode(&mut self, mode: Mode, value: bool) -> Result<&mut Self> {
let result =
unsafe { ffi::ghostty_terminal_mode_set(self.inner.as_raw(), mode.into(), value) };
from_result(result)?;
Ok(self)
}
pub(crate) fn get<T>(&self, tag: ffi::TerminalData::Type) -> Result<T> {
let mut value = MaybeUninit::<T>::zeroed();
let result = unsafe {
ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
};
from_result(result)?;
Ok(unsafe { value.assume_init() })
}
pub(crate) fn get_optional<T>(&self, tag: ffi::TerminalData::Type) -> Result<Option<T>> {
let mut value = MaybeUninit::<T>::zeroed();
let result = unsafe {
ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
};
from_optional_result_uninit(result, value)
}
pub(crate) fn set<T>(&self, tag: ffi::TerminalOption::Type, v: &T) -> Result<()> {
let result = unsafe {
ffi::ghostty_terminal_set(self.inner.as_raw(), tag, std::ptr::from_ref(v).cast())
};
from_result(result)
}
pub(crate) fn set_ptr(
&self,
tag: ffi::TerminalOption::Type,
ptr: *const std::ffi::c_void,
) -> Result<()> {
let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr) };
from_result(result)
}
pub(crate) fn set_optional<T>(
&self,
tag: ffi::TerminalOption::Type,
v: Option<&T>,
) -> Result<()> {
let ptr = if let Some(v) = v {
std::ptr::from_ref(v)
} else {
std::ptr::null()
};
let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr.cast()) };
from_result(result)
}
pub fn cols(&self) -> Result<u16> {
self.get(Data::COLS)
}
pub fn rows(&self) -> Result<u16> {
self.get(Data::ROWS)
}
pub fn cursor_x(&self) -> Result<u16> {
self.get(Data::CURSOR_X)
}
pub fn cursor_y(&self) -> Result<u16> {
self.get(Data::CURSOR_Y)
}
pub fn is_cursor_pending_wrap(&self) -> Result<bool> {
self.get(Data::CURSOR_PENDING_WRAP)
}
pub fn is_cursor_visible(&self) -> Result<bool> {
self.get(Data::CURSOR_VISIBLE)
}
pub fn cursor_style(&self) -> Result<style::Style> {
self.get::<ffi::Style>(Data::CURSOR_STYLE)
.and_then(std::convert::TryInto::try_into)
}
pub fn kitty_keyboard_flags(&self) -> Result<key::KittyKeyFlags> {
self.get::<ffi::KittyKeyFlags>(Data::KITTY_KEYBOARD_FLAGS)
.map(key::KittyKeyFlags::from_bits_retain)
}
pub fn scrollbar(&self) -> Result<Scrollbar> {
self.get(Data::SCROLLBAR)
}
pub fn active_screen(&self) -> Result<Screen> {
self.get(Data::ACTIVE_SCREEN)
}
pub fn is_mouse_tracking(&self) -> Result<bool> {
self.get(Data::MOUSE_TRACKING)
}
pub fn title(&self) -> Result<&str> {
let str = self.get::<ffi::String>(Data::TITLE)?;
let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
}
pub fn pwd(&self) -> Result<&str> {
let str = self.get::<ffi::String>(Data::PWD)?;
let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
}
pub fn total_rows(&self) -> Result<usize> {
self.get(Data::TOTAL_ROWS)
}
pub fn scrollback_rows(&self) -> Result<usize> {
self.get(Data::SCROLLBACK_ROWS)
}
pub fn fg_color(&self) -> Result<Option<RgbColor>> {
self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND)
.map(|v| v.map(Into::into))
}
pub fn default_fg_color(&self) -> Result<Option<RgbColor>> {
self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND_DEFAULT)
.map(|v| v.map(Into::into))
}
pub fn set_default_fg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
self.set_optional(Opt::COLOR_FOREGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
Ok(self)
}
pub fn bg_color(&self) -> Result<Option<RgbColor>> {
self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND)
.map(|v| v.map(Into::into))
}
pub fn default_bg_color(&self) -> Result<Option<RgbColor>> {
self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND_DEFAULT)
.map(|v| v.map(Into::into))
}
pub fn set_default_bg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
self.set_optional(Opt::COLOR_BACKGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
Ok(self)
}
pub fn cursor_color(&self) -> Result<Option<RgbColor>> {
self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR)
.map(|v| v.map(Into::into))
}
pub fn default_cursor_color(&self) -> Result<Option<RgbColor>> {
self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR_DEFAULT)
.map(|v| v.map(Into::into))
}
pub fn set_default_cursor_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
self.set_optional(Opt::COLOR_CURSOR, v.map(ffi::ColorRgb::from).as_ref())?;
Ok(self)
}
pub fn set_default_cursor_style(&mut self, v: Option<CursorStyle>) -> Result<&mut Self> {
self.set_optional(Opt::DEFAULT_CURSOR_STYLE, v.as_ref())?;
Ok(self)
}
pub fn set_default_cursor_blink(&mut self, v: Option<bool>) -> Result<&mut Self> {
self.set_optional(Opt::DEFAULT_CURSOR_BLINK, v.as_ref())?;
Ok(self)
}
pub fn color_palette(&self) -> Result<[RgbColor; 256]> {
self.get::<[ffi::ColorRgb; 256]>(Data::COLOR_PALETTE)
.map(|v| v.map(Into::into))
}
pub fn default_color_palette(&self) -> Result<[RgbColor; 256]> {
self.get::<[ffi::ColorRgb; 256]>(Data::COLOR_PALETTE_DEFAULT)
.map(|v| v.map(Into::into))
}
pub fn set_default_color_palette(&mut self, v: Option<[RgbColor; 256]>) -> Result<&mut Self> {
self.set_optional(
Opt::COLOR_PALETTE,
v.map(|v| v.map(ffi::ColorRgb::from)).as_ref(),
)?;
Ok(self)
}
pub fn set_apc_max_bytes(&mut self, max: Option<usize>) -> Result<&mut Self> {
self.set_optional(ffi::TerminalOption::APC_MAX_BYTES, max.as_ref())?;
Ok(self)
}
pub fn set_glyph_protocol_enabled(&mut self, enabled: bool) -> Result<&mut Self> {
self.set(ffi::TerminalOption::GLYPH_PROTOCOL, &enabled)?;
Ok(self)
}
}
impl Drop for Terminal<'_, '_> {
fn drop(&mut self) {
unsafe { ffi::ghostty_terminal_free(self.inner.as_raw()) }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Point {
Active(PointCoordinate),
Viewport(PointCoordinate),
Screen(PointCoordinate),
History(PointCoordinate),
}
impl From<Point> for ffi::Point {
fn from(value: Point) -> Self {
match value {
Point::Active(coord) => Self {
tag: ffi::PointTag::ACTIVE,
value: ffi::PointValue {
coordinate: coord.into(),
},
},
Point::Viewport(coord) => Self {
tag: ffi::PointTag::VIEWPORT,
value: ffi::PointValue {
coordinate: coord.into(),
},
},
Point::Screen(coord) => Self {
tag: ffi::PointTag::SCREEN,
value: ffi::PointValue {
coordinate: coord.into(),
},
},
Point::History(coord) => Self {
tag: ffi::PointTag::HISTORY,
value: ffi::PointValue {
coordinate: coord.into(),
},
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointSpace {
Active,
Viewport,
Screen,
History,
}
impl PointSpace {
pub(crate) fn into_raw(self) -> ffi::PointTag::Type {
match self {
Self::Active => ffi::PointTag::ACTIVE,
Self::Viewport => ffi::PointTag::VIEWPORT,
Self::Screen => ffi::PointTag::SCREEN,
Self::History => ffi::PointTag::HISTORY,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PointCoordinate {
pub x: u16,
pub y: u32,
}
impl From<PointCoordinate> for ffi::PointCoordinate {
fn from(value: PointCoordinate) -> Self {
let PointCoordinate { x, y } = value;
Self { x, y }
}
}
impl From<ffi::PointCoordinate> for PointCoordinate {
fn from(value: ffi::PointCoordinate) -> Self {
let ffi::PointCoordinate { x, y } = value;
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollViewport {
Top,
Bottom,
Delta(isize),
}
impl From<ScrollViewport> for ffi::TerminalScrollViewport {
fn from(value: ScrollViewport) -> Self {
match value {
ScrollViewport::Top => Self {
tag: ffi::TerminalScrollViewportTag::TOP,
value: ffi::TerminalScrollViewportValue::default(),
},
ScrollViewport::Bottom => Self {
tag: ffi::TerminalScrollViewportTag::BOTTOM,
value: ffi::TerminalScrollViewportValue::default(),
},
ScrollViewport::Delta(delta) => Self {
tag: ffi::TerminalScrollViewportTag::DELTA,
value: {
let mut v = ffi::TerminalScrollViewportValue::default();
v.delta = delta;
v
},
},
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Mode(pub ffi::Mode);
impl Mode {
#![expect(missing_docs, reason = "no upstream documentation provided")]
const ANSI_BIT: u16 = 1 << 15;
#[must_use]
pub const fn new(v: u16, kind: ModeKind) -> Self {
match kind {
ModeKind::Ansi => Self(v | Self::ANSI_BIT),
ModeKind::Dec => Self(v),
}
}
#[must_use]
pub const fn value(self) -> u16 {
(self.0) & 0x7fff
}
#[must_use]
pub const fn kind(self) -> ModeKind {
if (self.0) & Self::ANSI_BIT > 0 {
ModeKind::Ansi
} else {
ModeKind::Dec
}
}
pub const KAM: Self = Self::new(2, ModeKind::Ansi);
pub const INSERT: Self = Self::new(4, ModeKind::Ansi);
pub const SRM: Self = Self::new(12, ModeKind::Ansi);
pub const LINEFEED: Self = Self::new(20, ModeKind::Ansi);
pub const DECCKM: Self = Self::new(1, ModeKind::Dec);
pub const _132_COLUMN: Self = Self::new(3, ModeKind::Dec);
pub const SLOW_SCROLL: Self = Self::new(4, ModeKind::Dec);
pub const REVERSE_COLORS: Self = Self::new(5, ModeKind::Dec);
pub const ORIGIN: Self = Self::new(6, ModeKind::Dec);
pub const WRAPAROUND: Self = Self::new(7, ModeKind::Dec);
pub const AUTOREPEAT: Self = Self::new(8, ModeKind::Dec);
pub const X10_MOUSE: Self = Self::new(9, ModeKind::Dec);
pub const CURSOR_BLINKING: Self = Self::new(12, ModeKind::Dec);
pub const CURSOR_VISIBLE: Self = Self::new(25, ModeKind::Dec);
pub const ENABLE_MODE3: Self = Self::new(40, ModeKind::Dec);
pub const REVERSE_WRAP: Self = Self::new(45, ModeKind::Dec);
pub const ALT_SCREEN_LEGACY: Self = Self::new(47, ModeKind::Dec);
pub const KEYPAD_KEYS: Self = Self::new(66, ModeKind::Dec);
pub const LEFT_RIGHT_MARGIN: Self = Self::new(69, ModeKind::Dec);
pub const NORMAL_MOUSE: Self = Self::new(1000, ModeKind::Dec);
pub const BUTTON_MOUSE: Self = Self::new(1002, ModeKind::Dec);
pub const ANY_MOUSE: Self = Self::new(1003, ModeKind::Dec);
pub const FOCUS_EVENT: Self = Self::new(1004, ModeKind::Dec);
pub const UTF8_MOUSE: Self = Self::new(1005, ModeKind::Dec);
pub const SGR_MOUSE: Self = Self::new(1006, ModeKind::Dec);
pub const ALT_SCROLL: Self = Self::new(1007, ModeKind::Dec);
pub const URXVT_MOUSE: Self = Self::new(1015, ModeKind::Dec);
pub const SGR_PIXELS_MOUSE: Self = Self::new(1016, ModeKind::Dec);
pub const NUMLOCK_KEYPAD: Self = Self::new(1035, ModeKind::Dec);
pub const ALT_ESC_PREFIX: Self = Self::new(1036, ModeKind::Dec);
pub const ALT_SENDS_ESC: Self = Self::new(1039, ModeKind::Dec);
pub const REVERSE_WRAP_EXT: Self = Self::new(1045, ModeKind::Dec);
pub const ALT_SCREEN: Self = Self::new(1047, ModeKind::Dec);
pub const SAVE_CURSOR: Self = Self::new(1048, ModeKind::Dec);
pub const ALT_SCREEN_SAVE: Self = Self::new(1049, ModeKind::Dec);
pub const BRACKETED_PASTE: Self = Self::new(2004, ModeKind::Dec);
pub const SYNC_OUTPUT: Self = Self::new(2026, ModeKind::Dec);
pub const GRAPHEME_CLUSTER: Self = Self::new(2027, ModeKind::Dec);
pub const COLOR_SCHEME_REPORT: Self = Self::new(2031, ModeKind::Dec);
pub const IN_BAND_RESIZE: Self = Self::new(2048, ModeKind::Dec);
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ModeKind {
Dec,
Ansi,
}
impl From<Mode> for ffi::Mode {
fn from(value: Mode) -> Self {
value.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct DeviceAttributes {
pub primary: PrimaryDeviceAttributes,
pub secondary: SecondaryDeviceAttributes,
pub tertiary: TertiaryDeviceAttributes,
}
impl From<DeviceAttributes> for ffi::DeviceAttributes {
fn from(value: DeviceAttributes) -> Self {
Self {
primary: value.primary.into(),
secondary: value.secondary.into(),
tertiary: value.tertiary.into(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PrimaryDeviceAttributes(ffi::DeviceAttributesPrimary);
impl PrimaryDeviceAttributes {
#[must_use]
pub const fn new(
conformance_level: ConformanceLevel,
features: &[DeviceAttributeFeature],
) -> Self {
assert!(features.len() <= 64);
let mut f = [0u16; 64];
let mut i = 0;
while i < features.len() {
f[i] = features[i].0;
i += 1;
}
Self(ffi::DeviceAttributesPrimary {
conformance_level: conformance_level.0,
features: f,
num_features: features.len(),
})
}
}
impl From<PrimaryDeviceAttributes> for ffi::DeviceAttributesPrimary {
fn from(value: PrimaryDeviceAttributes) -> Self {
value.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConformanceLevel(pub u16);
impl ConformanceLevel {
#![expect(clippy::doc_markdown, reason = "false positive")]
#![expect(missing_docs, reason = "self-explanatory")]
pub const VT100: Self = Self(ffi::DA_CONFORMANCE_VT100);
pub const VT101: Self = Self(ffi::DA_CONFORMANCE_VT101);
pub const VT102: Self = Self(ffi::DA_CONFORMANCE_VT102);
pub const VT125: Self = Self(ffi::DA_CONFORMANCE_VT125);
pub const VT131: Self = Self(ffi::DA_CONFORMANCE_VT131);
pub const VT132: Self = Self(ffi::DA_CONFORMANCE_VT132);
pub const VT220: Self = Self(ffi::DA_CONFORMANCE_VT220);
pub const VT240: Self = Self(ffi::DA_CONFORMANCE_VT240);
pub const VT320: Self = Self(ffi::DA_CONFORMANCE_VT320);
pub const VT340: Self = Self(ffi::DA_CONFORMANCE_VT340);
pub const VT420: Self = Self(ffi::DA_CONFORMANCE_VT420);
pub const VT510: Self = Self(ffi::DA_CONFORMANCE_VT510);
pub const VT520: Self = Self(ffi::DA_CONFORMANCE_VT520);
pub const VT525: Self = Self(ffi::DA_CONFORMANCE_VT525);
pub const LEVEL_2: Self = Self(ffi::DA_CONFORMANCE_LEVEL_2);
pub const LEVEL_3: Self = Self(ffi::DA_CONFORMANCE_LEVEL_3);
pub const LEVEL_4: Self = Self(ffi::DA_CONFORMANCE_LEVEL_4);
pub const LEVEL_5: Self = Self(ffi::DA_CONFORMANCE_LEVEL_5);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DeviceAttributeFeature(pub u16);
impl DeviceAttributeFeature {
#![expect(missing_docs, reason = "no upstream documentation provided")]
pub const COLUMNS_132: Self = Self(ffi::DA_FEATURE_COLUMNS_132);
pub const PRINTER: Self = Self(ffi::DA_FEATURE_PRINTER);
pub const REGIS: Self = Self(ffi::DA_FEATURE_REGIS);
pub const SIXEL: Self = Self(ffi::DA_FEATURE_SIXEL);
pub const SELECTIVE_ERASE: Self = Self(ffi::DA_FEATURE_SELECTIVE_ERASE);
pub const USER_DEFINED_KEYS: Self = Self(ffi::DA_FEATURE_USER_DEFINED_KEYS);
pub const NATIONAL_REPLACEMENT: Self = Self(ffi::DA_FEATURE_NATIONAL_REPLACEMENT);
pub const TECHNICAL_CHARACTERS: Self = Self(ffi::DA_FEATURE_TECHNICAL_CHARACTERS);
pub const LOCATOR: Self = Self(ffi::DA_FEATURE_LOCATOR);
pub const TERMINAL_STATE: Self = Self(ffi::DA_FEATURE_TERMINAL_STATE);
pub const WINDOWING: Self = Self(ffi::DA_FEATURE_WINDOWING);
pub const HORIZONTAL_SCROLLING: Self = Self(ffi::DA_FEATURE_HORIZONTAL_SCROLLING);
pub const ANSI_COLOR: Self = Self(ffi::DA_FEATURE_ANSI_COLOR);
pub const RECTANGULAR_EDITING: Self = Self(ffi::DA_FEATURE_RECTANGULAR_EDITING);
pub const ANSI_TEXT_LOCATOR: Self = Self(ffi::DA_FEATURE_ANSI_TEXT_LOCATOR);
pub const CLIPBOARD: Self = Self(ffi::DA_FEATURE_CLIPBOARD);
}
#[derive(Debug, Copy, Clone)]
pub struct SecondaryDeviceAttributes {
pub device_type: DeviceType,
pub firmware_version: u16,
pub rom_cartridge: u16,
}
impl From<SecondaryDeviceAttributes> for ffi::DeviceAttributesSecondary {
fn from(value: SecondaryDeviceAttributes) -> Self {
Self {
device_type: value.device_type.0,
firmware_version: value.firmware_version,
rom_cartridge: value.rom_cartridge,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DeviceType(pub u16);
impl DeviceType {
#![expect(missing_docs, reason = "self-explanatory")]
pub const VT100: Self = Self(ffi::DA_DEVICE_TYPE_VT100);
pub const VT220: Self = Self(ffi::DA_DEVICE_TYPE_VT220);
pub const VT240: Self = Self(ffi::DA_DEVICE_TYPE_VT240);
pub const VT330: Self = Self(ffi::DA_DEVICE_TYPE_VT330);
pub const VT340: Self = Self(ffi::DA_DEVICE_TYPE_VT340);
pub const VT320: Self = Self(ffi::DA_DEVICE_TYPE_VT320);
pub const VT382: Self = Self(ffi::DA_DEVICE_TYPE_VT382);
pub const VT420: Self = Self(ffi::DA_DEVICE_TYPE_VT420);
pub const VT510: Self = Self(ffi::DA_DEVICE_TYPE_VT510);
pub const VT520: Self = Self(ffi::DA_DEVICE_TYPE_VT520);
pub const VT525: Self = Self(ffi::DA_DEVICE_TYPE_VT525);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct TertiaryDeviceAttributes {
pub unit_id: u32,
}
impl From<TertiaryDeviceAttributes> for ffi::DeviceAttributesTertiary {
fn from(value: TertiaryDeviceAttributes) -> Self {
Self {
unit_id: value.unit_id,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
#[expect(missing_docs, reason = "self-explanatory")]
pub enum ColorScheme {
Light = ffi::ColorScheme::LIGHT,
Dark = ffi::ColorScheme::DARK,
}
macro_rules! handlers {
{
$(
$(#[$fmeta:meta])*
$vis:vis fn $name:ident(
&mut self,
tag = $tag:ident,
from = $rawfnty:ident( $($rfname:ident: $rfty:ty),*$(,)? ) $(-> $rawrty:ty)?,
$(#[$tmeta:meta])*
to = $(<$lf:lifetime>)? $fnty:ident( $($fty:ty),*$(,)? ) $(-> $rty:ty)?,
) |$t:ident, $func:ident| $block:block
)*
} => {
impl<'alloc, 'cb> $crate::terminal::Terminal<'alloc, 'cb> {$(
$(#[$fmeta])*
$vis fn $name(&mut self, f: impl $fnty<'alloc, 'cb>) -> $crate::error::Result<&mut Self> {
unsafe extern "C" fn callback(
t: $crate::ffi::Terminal,
ud: *mut std::ffi::c_void,
$($rfname: $rfty),*
) $(-> $rawrty)? {
let vtable = unsafe { &mut *ud.cast::<VTable<'_, '_>>() };
let obj = $crate::alloc::Object::new(t).expect("received null terminal ptr in callback - this is a bug!");
let mut term = ::core::mem::ManuallyDrop::new($crate::terminal::Terminal::<'_, '_> {
inner: obj,
vtable: ::core::default::Default::default(),
});
let $t: &$crate::terminal::Terminal = &term;
let $func = vtable.$name.as_deref_mut()
.expect("no handler set but callback is still called - this is a bug!");
let ret = $block;
unsafe { ::core::ptr::drop_in_place(&mut term.vtable) };
ret
}
self.vtable.$name = Some(::std::boxed::Box::new(f));
let userdata = std::ptr::from_mut::<VTable<'alloc, 'cb>>(self.vtable.as_mut())
as *const ::std::ffi::c_void;
self.set_ptr($crate::ffi::TerminalOption::USERDATA, userdata)?;
let callback_ptr: unsafe extern "C" fn(
$crate::ffi::Terminal,
*mut ::std::ffi::c_void,
$($rfty),*
) $(-> $rawrty)? = callback;
let result = unsafe {
$crate::ffi::ghostty_terminal_set(
self.inner.as_raw(),
$crate::ffi::TerminalOption::$tag,
callback_ptr as *const ::std::ffi::c_void
)
};
$crate::error::from_result(result)?;
Ok(self)
}
)*}
$(
#[doc = concat!(
"[Effect](Terminal#effects) callback type for [`Terminal::",
stringify!($name),
"`](Terminal::",
stringify!($name),
").\n"
)]
$(#[$tmeta])*
pub trait $fnty<'alloc, 'cb>:
$(for<$lf>)? FnMut(
&$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
$($fty),*
) $(-> $rty)? + 'cb {}
impl<'alloc, 'cb, F> $fnty<'alloc, 'cb> for F
where
F: $(for<$lf>)? FnMut(
&$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
$($fty),*
) $(-> $rty)? + 'cb
{}
)*
struct VTable<'alloc, 'cb> {
$($name: Option<::std::boxed::Box<dyn $fnty<'alloc, 'cb>>>),*
}
impl ::core::fmt::Debug for VTable<'_, '_> {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
f.write_str("VTable {..}")
}
}
impl ::core::default::Default for VTable<'_, '_> {
fn default() -> Self {
Self {
$($name: None),*
}
}
}
};
}
handlers! {
pub fn on_pty_write(
&mut self,
tag = WRITE_PTY,
from = GhosttyTerminalWritePtyFn(ptr: *const u8, len: usize),
to = <'t>PtyWriteFn(&'t [u8]),
) |term, func| {
let data = unsafe { std::slice::from_raw_parts(ptr, len) };
func(&term, data);
}
pub fn on_bell(
&mut self,
tag = BELL,
from = GhosttyTerminalBellFn(),
to = BellFn(),
) |term, func| {
func(&term);
}
pub fn on_enquiry(
&mut self,
tag = ENQUIRY,
from = GhosttyTerminalEnquiryFn() -> ffi::String,
to = <'t>EnquiryFn() -> Option<&'t str>,
) |term, func| {
func(&term).unwrap_or("").into()
}
pub fn on_xtversion(
&mut self,
tag = XTVERSION,
from = GhosttyTerminalXtversionFn() -> ffi::String,
to = <'t>XtversionFn() -> Option<&'t str>,
) |term, func| {
func(&term).unwrap_or("").into()
}
pub fn on_title_changed(
&mut self,
tag = TITLE_CHANGED,
from = GhosttyTerminalTitleChangedFn(),
to = TitleChangedFn(),
) |term, func| {
func(&term);
}
pub fn on_pwd_changed(
&mut self,
tag = PWD_CHANGED,
from = GhosttyTerminalPwdChangedFn(),
to = PwdChangedFn(),
) |term, func| {
func(&term);
}
pub fn on_size(
&mut self,
tag = SIZE,
from = GhosttyTerminalSizeFn(out: *mut ffi::SizeReportSize) -> bool,
to = SizeFn() -> Option<SizeReportSize>,
) |term, func| {
if let Some(size) = func(&term) {
unsafe { *out = size };
true
} else {
false
}
}
pub fn on_color_scheme(
&mut self,
tag = COLOR_SCHEME,
from = GhosttyTerminalColorSchemeFn(out: *mut ffi::ColorScheme::Type) -> bool,
to = ColorSchemeFn() -> Option<ColorScheme>,
) |term, func| {
if let Some(size) = func(&term) {
unsafe { *out = size as ffi::ColorScheme::Type };
true
} else {
false
}
}
pub fn on_device_attributes(
&mut self,
tag = DEVICE_ATTRIBUTES,
from = GhosttyTerminalDeviceAttributesFn(out: *mut ffi::DeviceAttributes) -> bool,
to = DeviceAttributesFn() -> Option<DeviceAttributes>,
) |term, func| {
if let Some(size) = func(&term) {
unsafe { *out = size.into() };
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RenderState;
use crate::render::CursorVisualStyle;
use std::cell::{Cell, RefCell};
use std::mem::ManuallyDrop;
#[inline(never)]
fn build_terminal<'cb>(callback_count: &'cb RefCell<usize>) -> Terminal<'static, 'cb> {
let mut terminal = Terminal::new(Options {
cols: 80,
rows: 24,
max_scrollback: 1000,
})
.expect("terminal should initialize");
terminal
.on_device_attributes(move |_term| {
*callback_count.borrow_mut() += 1;
Some(DeviceAttributes {
primary: PrimaryDeviceAttributes::new(
ConformanceLevel::VT220,
&[DeviceAttributeFeature::ANSI_COLOR],
),
secondary: SecondaryDeviceAttributes {
device_type: DeviceType::VT220,
firmware_version: 1,
rom_cartridge: 0,
},
tertiary: TertiaryDeviceAttributes { unit_id: 0 },
})
})
.expect("callback should register");
terminal
}
fn relocate_into_new_box<T>(value: T) -> (Box<T>, usize, usize) {
let src = Box::new(ManuallyDrop::new(value));
let src_addr = std::ptr::from_ref(&**src).cast::<T>() as usize;
unsafe {
let dst_layout = std::alloc::Layout::new::<T>();
let dst_ptr = std::alloc::alloc(dst_layout).cast::<T>();
if dst_ptr.is_null() {
std::alloc::handle_alloc_error(dst_layout);
}
let dst_addr = dst_ptr as usize;
assert_ne!(
src_addr, dst_addr,
"test setup failed: source and destination storage unexpectedly match"
);
std::ptr::copy_nonoverlapping(std::ptr::from_ref(&**src).cast::<T>(), dst_ptr, 1);
std::alloc::dealloc(
Box::into_raw(src).cast::<u8>(),
std::alloc::Layout::new::<ManuallyDrop<T>>(),
);
(Box::from_raw(dst_ptr), src_addr, dst_addr)
}
}
#[test]
fn title_changed_callback_returns_correct_title() {
let captured_title: RefCell<String> = RefCell::new(String::new());
let callback_count: Cell<usize> = Cell::new(0);
let mut terminal = Terminal::new(Options {
cols: 80,
rows: 24,
max_scrollback: 0,
})
.expect("terminal should initialize");
terminal
.on_title_changed(|term| {
callback_count.set(callback_count.get() + 1);
let title = term
.title()
.expect("title() should succeed inside callback");
*captured_title.borrow_mut() = title.to_owned();
})
.expect("callback should register");
terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
assert_eq!(callback_count.get(), 1);
assert_eq!(*captured_title.borrow(), "Hello Effects");
terminal.vt_write(b"\x1b]2;Second Title\x1b\\");
assert_eq!(callback_count.get(), 2);
assert_eq!(*captured_title.borrow(), "Second Title");
}
#[test]
fn pwd_changed_callback_returns_correct_pwd() {
let captured_pwd: RefCell<String> = RefCell::new(String::new());
let callback_count: Cell<usize> = Cell::new(0);
let mut terminal = Terminal::new(Options {
cols: 80,
rows: 24,
max_scrollback: 0,
})
.expect("terminal should initialize");
terminal
.on_pwd_changed(|term| {
callback_count.set(callback_count.get() + 1);
let pwd = term.pwd().expect("pwd() should succeed inside callback");
*captured_pwd.borrow_mut() = pwd.to_owned();
})
.expect("callback should register");
terminal.vt_write(b"\x1b]7;file://localhost/tmp/project\x1b\\");
assert_eq!(callback_count.get(), 1);
assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/project");
terminal.vt_write(b"\x1b]7;file://localhost/tmp/other\x1b\\");
assert_eq!(callback_count.get(), 2);
assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/other");
}
#[test]
fn default_cursor_reset_uses_configured_style_and_blink() {
let mut terminal = Terminal::new(Options {
cols: 80,
rows: 24,
max_scrollback: 0,
})
.expect("terminal should initialize");
let mut render_state = RenderState::new().expect("render state should initialize");
terminal
.set_default_cursor_style(Some(CursorStyle::Underline))
.expect("default cursor style should update")
.set_default_cursor_blink(Some(true))
.expect("default cursor blink should update");
terminal.vt_write(b"\x1b[0 q");
let snapshot = render_state
.update(&terminal)
.expect("render state should update");
assert_eq!(
snapshot
.cursor_visual_style()
.expect("cursor style should be readable"),
CursorVisualStyle::Underline
);
assert!(
snapshot
.cursor_blinking()
.expect("cursor blink should be readable")
);
}
#[test]
fn glyph_protocol_enabled_setting_updates() {
let mut terminal = Terminal::new(Options {
cols: 80,
rows: 24,
max_scrollback: 0,
})
.expect("terminal should initialize");
terminal
.set_glyph_protocol_enabled(false)
.expect("glyph protocol should disable")
.set_glyph_protocol_enabled(true)
.expect("glyph protocol should enable");
}
#[test]
fn callbacks_survive_explicit_relocation() {
let callback_count = RefCell::new(0usize);
let terminal = build_terminal(&callback_count);
let (mut terminal, addr_before, addr_after) = relocate_into_new_box(terminal);
assert_ne!(addr_before, addr_after);
terminal.vt_write(b"\x1b[c");
assert_eq!(*callback_count.borrow(), 1);
}
fn tiny_terminal() -> Terminal<'static, 'static> {
Terminal::new(Options {
cols: 8,
rows: 3,
max_scrollback: 100,
})
.expect("terminal should initialize")
}
fn codepoint_at_tracked_ref(terminal: &Terminal<'_, '_>, tracked: &TrackedGridRef) -> u32 {
let snapshot = tracked
.snapshot(terminal)
.expect("tracked snapshot should not fail")
.expect("tracked ref should have a value");
snapshot
.cell()
.expect("tracked snapshot should resolve to a cell")
.codepoint()
.expect("tracked snapshot cell should expose a codepoint")
}
#[test]
fn tracked_grid_ref_follows_scroll() {
let mut terminal = tiny_terminal();
terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
let tracked = terminal
.track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
.expect("tracked grid ref should initialize");
terminal.vt_write(b"\r\ndelta");
assert!(tracked.has_value());
assert_eq!(
codepoint_at_tracked_ref(&terminal, &tracked),
u32::from('a')
);
assert_eq!(
tracked
.point(PointSpace::Screen)
.expect("tracked point should resolve")
.expect("tracked point should have a value")
.x,
0
);
}
#[test]
fn tracked_grid_ref_reports_loss_and_can_set_point() {
let mut terminal = tiny_terminal();
terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
let mut tracked = terminal
.track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
.expect("tracked grid ref should initialize");
terminal.reset();
assert!(!tracked.has_value());
assert!(
tracked
.snapshot(&terminal)
.expect("missing tracked snapshot should not fail")
.is_none()
);
assert!(
tracked
.point(PointSpace::Screen)
.expect("missing tracked point should not fail")
.is_none()
);
terminal.vt_write(b"echo");
tracked
.set(&mut terminal, Point::Active(PointCoordinate { x: 0, y: 0 }))
.expect("tracked grid ref should set to a new point");
assert!(tracked.has_value());
assert_eq!(
codepoint_at_tracked_ref(&terminal, &tracked),
u32::from('e')
);
}
#[test]
fn tracked_grid_ref_survives_terminal_drop() {
let tracked = {
let mut terminal = tiny_terminal();
terminal.vt_write(b"alpha");
terminal
.track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
.expect("tracked grid ref should initialize")
};
assert!(!tracked.has_value());
assert!(
tracked
.point(PointSpace::Screen)
.expect("detached tracked point should not fail")
.is_none()
);
}
#[test]
fn tracked_grid_ref_rejects_different_terminal() {
let mut first = tiny_terminal();
first.vt_write(b"alpha");
let mut second = tiny_terminal();
second.vt_write(b"bravo");
let mut tracked = first
.track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
.expect("tracked grid ref should initialize");
assert!(matches!(
tracked.snapshot(&second),
Err(Error::InvalidValue)
));
assert!(matches!(
tracked.set(&mut second, Point::Active(PointCoordinate { x: 0, y: 0 })),
Err(Error::InvalidValue)
));
}
#[test]
fn grid_ref_converts_back_to_point() {
let mut terminal = tiny_terminal();
terminal.vt_write(b"alpha");
let original = PointCoordinate { x: 1, y: 0 };
let grid_ref = terminal
.grid_ref(Point::Active(original))
.expect("grid ref should resolve");
assert_eq!(
terminal
.point_from_grid_ref(&grid_ref, PointSpace::Active)
.expect("grid ref point conversion should not fail")
.expect("grid ref should be representable in active space"),
original
);
}
}