#![allow(non_camel_case_types, dead_code)]
use std::os::raw::{c_int, c_void};
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU64, Ordering};
use std::time::Instant;
use parking_lot::Mutex;
fn perf_trace_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var_os("LINGXIA_GHOSTTY_PROFILE").is_some_and(|v| !v.is_empty() && v != "0")
})
}
fn perf_trace_verbose() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var_os("LINGXIA_GHOSTTY_PROFILE_VERBOSE")
.is_some_and(|v| !v.is_empty() && v != "0")
})
}
pub type GhosttyTerminal = *mut c_void;
pub type GhosttyRenderState = *mut c_void;
pub type GhosttyRowIterator = *mut c_void;
pub type GhosttyRowCells = *mut c_void;
pub type GhosttyAllocator = c_void;
pub type GhosttyResult = c_int;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum GhosttyTerminalData {
Invalid = 0,
Cols = 1,
Rows = 2,
CursorX = 3,
CursorY = 4,
CursorPendingWrap = 5,
ActiveScreen = 6,
CursorVisible = 7,
Scrollbar = 9,
Title = 12,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyTerminalScrollbar {
pub total: u64,
pub offset: u64,
pub len: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GhosttyScrollbar {
pub total: u64,
pub offset: u64,
pub len: u64,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhosttyTerminalScreen {
Primary = 0,
Alternate = 1,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhosttyTerminalScrollViewportTag {
Top = 0,
Bottom = 1,
Delta = 2,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union GhosttyTerminalScrollViewportValue {
pub delta: isize,
pub _padding: [u64; 2],
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct GhosttyTerminalScrollViewport {
pub tag: GhosttyTerminalScrollViewportTag,
pub value: GhosttyTerminalScrollViewportValue,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum GhosttyTerminalOption {
Userdata = 0,
WritePty = 1,
Bell = 2,
Enquiry = 3,
Xtversion = 4,
TitleChanged = 5,
Size = 6,
ColorScheme = 7,
DeviceAttributes = 8,
ColorForeground = 11,
ColorBackground = 12,
ColorCursor = 13,
ColorPalette = 14,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum GhosttyRenderStateData {
Invalid = 0,
Cols = 1,
Rows = 2,
Dirty = 3,
RowIterator = 4,
ColorBackground = 5,
ColorForeground = 6,
ColorCursor = 7,
ColorCursorHasValue = 8,
ColorPalette = 9,
CursorVisualStyle = 10,
CursorVisible = 11,
CursorBlinking = 12,
CursorPasswordInput = 13,
CursorViewportHasValue = 14,
CursorViewportX = 15,
CursorViewportY = 16,
CursorViewportWideTail = 17,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhosttyRenderStateDirty {
False = 0,
Partial = 1,
Full = 2,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GhosttyRenderStateCursorVisualStyle {
Bar = 0,
#[default]
Block = 1,
Underline = 2,
BlockHollow = 3,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum GhosttyRenderStateRowData {
Invalid = 0,
Dirty = 1,
Raw = 2,
Cells = 3,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum GhosttyRenderStateRowCellsData {
Invalid = 0,
Raw = 1,
Style = 2,
GraphemesLen = 3,
GraphemesBuf = 4,
BgColor = 5,
FgColor = 6,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum GhosttyCellData {
Invalid = 0,
Codepoint = 1,
ContentTag = 2,
Wide = 3,
HasText = 4,
HasStyling = 5,
StyleId = 6,
HasHyperlink = 7,
Protected = 8,
SemanticContent = 9,
ColorPalette = 10,
ColorRgb = 11,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GhosttyCellWide {
#[default]
Narrow = 0,
Wide = 1,
SpacerTail = 2,
SpacerHead = 3,
}
pub type GhosttyCell = u64;
pub type GhosttyMode = u16;
#[inline]
pub const fn ghostty_mode(value: u16, ansi: bool) -> GhosttyMode {
(value & 0x7FFF) | ((ansi as u16) << 15)
}
pub const MODE_NORMAL_MOUSE: GhosttyMode = ghostty_mode(1000, false);
pub const MODE_BUTTON_MOUSE: GhosttyMode = ghostty_mode(1002, false);
pub const MODE_ANY_MOUSE: GhosttyMode = ghostty_mode(1003, false);
pub const MODE_X10_MOUSE: GhosttyMode = ghostty_mode(9, false);
pub const MODE_SGR_MOUSE: GhosttyMode = ghostty_mode(1006, false);
pub const MODE_ALT_SCROLL: GhosttyMode = ghostty_mode(1007, false);
pub const MODE_BRACKETED_PASTE: GhosttyMode = ghostty_mode(2004, false);
pub const MODE_DECCKM: GhosttyMode = ghostty_mode(1, false);
const GHOSTTY_DA_CONFORMANCE_LEVEL_2: u16 = 62;
const GHOSTTY_DA_FEATURE_SELECTIVE_ERASE: u16 = 6;
const GHOSTTY_DA_FEATURE_WINDOWING: u16 = 18;
const GHOSTTY_DA_FEATURE_ANSI_COLOR: u16 = 22;
const GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING: u16 = 28;
const GHOSTTY_DA_FEATURE_CLIPBOARD: u16 = 52;
const GHOSTTY_DA_DEVICE_TYPE_VT220: u16 = 1;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GhosttyTerminalOptions {
pub cols: u16,
pub rows: u16,
pub max_scrollback: usize,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyColorRgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyString {
pub ptr: *const u8,
pub len: usize,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhosttyColorScheme {
Light = 0,
Dark = 1,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttySizeReportSize {
pub rows: u16,
pub columns: u16,
pub cell_width: u32,
pub cell_height: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GhosttyDeviceAttributesPrimary {
pub conformance_level: u16,
pub features: [u16; 64],
pub num_features: usize,
}
impl Default for GhosttyDeviceAttributesPrimary {
fn default() -> Self {
Self {
conformance_level: 0,
features: [0; 64],
num_features: 0,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyDeviceAttributesSecondary {
pub device_type: u16,
pub firmware_version: u16,
pub rom_cartridge: u16,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyDeviceAttributesTertiary {
pub unit_id: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyDeviceAttributes {
pub primary: GhosttyDeviceAttributesPrimary,
pub secondary: GhosttyDeviceAttributesSecondary,
pub tertiary: GhosttyDeviceAttributesTertiary,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct GhosttyStyleColor {
pub tag: u32,
pub _pad: u32,
pub value: u64,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GhosttyStyle {
pub size: usize,
pub fg_color: GhosttyStyleColor,
pub bg_color: GhosttyStyleColor,
pub underline_color: GhosttyStyleColor,
pub bold: bool,
pub italic: bool,
pub faint: bool,
pub blink: bool,
pub inverse: bool,
pub invisible: bool,
pub strikethrough: bool,
pub overline: bool,
pub underline: c_int,
}
impl GhosttyStyle {
fn new() -> Self {
Self {
size: std::mem::size_of::<GhosttyStyle>(),
fg_color: GhosttyStyleColor::default(),
bg_color: GhosttyStyleColor::default(),
underline_color: GhosttyStyleColor::default(),
bold: false,
italic: false,
faint: false,
blink: false,
inverse: false,
invisible: false,
strikethrough: false,
overline: false,
underline: 0,
}
}
}
pub const ATTR_BOLD: u8 = 1 << 0;
pub const ATTR_ITALIC: u8 = 1 << 1;
pub const ATTR_UNDERLINE: u8 = 1 << 2;
pub const ATTR_STRIKE: u8 = 1 << 3;
pub const ATTR_INVERSE: u8 = 1 << 4;
unsafe extern "C" {
pub fn ghostty_terminal_new(
allocator: *const GhosttyAllocator,
out_terminal: *mut GhosttyTerminal,
options: GhosttyTerminalOptions,
) -> GhosttyResult;
pub fn ghostty_terminal_free(terminal: GhosttyTerminal);
pub fn ghostty_terminal_resize(
terminal: GhosttyTerminal,
cols: u16,
rows: u16,
cell_width_px: u32,
cell_height_px: u32,
) -> GhosttyResult;
pub fn ghostty_terminal_set(
terminal: GhosttyTerminal,
option: GhosttyTerminalOption,
value: *const c_void,
) -> GhosttyResult;
pub fn ghostty_terminal_vt_write(terminal: GhosttyTerminal, data: *const u8, len: usize);
pub fn ghostty_terminal_scroll_viewport(
terminal: GhosttyTerminal,
behavior: GhosttyTerminalScrollViewport,
);
pub fn ghostty_terminal_get(
terminal: GhosttyTerminal,
key: GhosttyTerminalData,
out: *mut c_void,
) -> GhosttyResult;
pub fn ghostty_terminal_mode_get(
terminal: GhosttyTerminal,
mode: GhosttyMode,
out_value: *mut bool,
) -> GhosttyResult;
pub fn ghostty_render_state_new(
allocator: *const GhosttyAllocator,
out_state: *mut GhosttyRenderState,
) -> GhosttyResult;
pub fn ghostty_render_state_free(state: GhosttyRenderState);
pub fn ghostty_render_state_update(
state: GhosttyRenderState,
terminal: GhosttyTerminal,
) -> GhosttyResult;
pub fn ghostty_render_state_get(
state: GhosttyRenderState,
key: GhosttyRenderStateData,
out: *mut c_void,
) -> GhosttyResult;
pub fn ghostty_render_state_row_iterator_new(
allocator: *const GhosttyAllocator,
out_iter: *mut GhosttyRowIterator,
) -> GhosttyResult;
pub fn ghostty_render_state_row_iterator_free(iter: GhosttyRowIterator);
pub fn ghostty_render_state_row_iterator_next(iter: GhosttyRowIterator) -> bool;
pub fn ghostty_render_state_row_get(
iter: GhosttyRowIterator,
key: GhosttyRenderStateRowData,
out: *mut c_void,
) -> GhosttyResult;
pub fn ghostty_render_state_row_cells_new(
allocator: *const GhosttyAllocator,
out_cells: *mut GhosttyRowCells,
) -> GhosttyResult;
pub fn ghostty_render_state_row_cells_free(cells: GhosttyRowCells);
pub fn ghostty_render_state_row_cells_next(cells: GhosttyRowCells) -> bool;
pub fn ghostty_render_state_row_cells_get(
cells: GhosttyRowCells,
key: GhosttyRenderStateRowCellsData,
out: *mut c_void,
) -> GhosttyResult;
pub fn ghostty_cell_get(
cell: GhosttyCell,
key: GhosttyCellData,
out: *mut c_void,
) -> GhosttyResult;
}
#[derive(Debug, Clone)]
pub struct ThemeColors {
pub fg: [u8; 3],
pub bg: [u8; 3],
pub palette: [[u8; 3]; 256],
}
impl ThemeColors {
pub fn from_ansi16(fg: [u8; 3], bg: [u8; 3], ansi16: [[u8; 3]; 16]) -> Self {
let mut palette = [[0u8; 3]; 256];
for i in 0..16 {
palette[i] = ansi16[i];
}
let step = |x: u8| -> u8 { if x == 0 { 0 } else { 55 + 40 * x } };
for i in 16..232 {
let idx = (i - 16) as u8;
let r = idx / 36;
let g = (idx / 6) % 6;
let b = idx % 6;
palette[i] = [step(r), step(g), step(b)];
}
for i in 232..256 {
let v = 8u8.saturating_add(((i - 232) as u8).saturating_mul(10));
palette[i] = [v, v, v];
}
Self { fg, bg, palette }
}
}
unsafe fn apply_theme_to_terminal(terminal: GhosttyTerminal, theme: &ThemeColors) {
let fg = GhosttyColorRgb {
r: theme.fg[0],
g: theme.fg[1],
b: theme.fg[2],
};
let bg = GhosttyColorRgb {
r: theme.bg[0],
g: theme.bg[1],
b: theme.bg[2],
};
let palette: [GhosttyColorRgb; 256] = std::array::from_fn(|i| GhosttyColorRgb {
r: theme.palette[i][0],
g: theme.palette[i][1],
b: theme.palette[i][2],
});
let check = |rc: GhosttyResult, what: &'static str| {
if rc != 0 {
eprintln!("ghostty_terminal_set({what}) failed: rc={rc}");
}
};
unsafe {
check(
ghostty_terminal_set(
terminal,
GhosttyTerminalOption::ColorForeground,
&fg as *const _ as *const c_void,
),
"ColorForeground",
);
check(
ghostty_terminal_set(
terminal,
GhosttyTerminalOption::ColorBackground,
&bg as *const _ as *const c_void,
),
"ColorBackground",
);
check(
ghostty_terminal_set(
terminal,
GhosttyTerminalOption::ColorPalette,
palette.as_ptr() as *const c_void,
),
"ColorPalette",
);
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Cell {
pub text: String,
pub fg: u32,
pub bg: u32,
pub attrs: u8,
pub wide: bool,
pub _pad: [u8; 2],
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cursor {
pub col: u16,
pub row: u16,
pub visible: bool,
pub style: GhosttyRenderStateCursorVisualStyle,
}
#[derive(Debug, Clone, Default)]
pub struct ScreenSnapshot {
pub cols: u16,
pub rows: u16,
pub cells: Vec<Cell>,
pub dirty_rows: Vec<u16>,
pub cursor: Cursor,
pub default_fg: u32,
pub default_bg: u32,
pub title: Option<String>,
pub generation: u64,
}
pub struct VtScreen {
inner: Arc<Mutex<VtInner>>,
}
pub type PtyWriteCallback = Arc<dyn Fn(&[u8]) + Send + Sync + 'static>;
struct VtCallbackState {
write_pty: PtyWriteCallback,
enquiry_response: Box<[u8]>,
rows: AtomicU16,
cols: AtomicU16,
cell_width: AtomicU32,
cell_height: AtomicU32,
dark_mode: AtomicBool,
device_attributes: GhosttyDeviceAttributes,
}
struct VtInner {
terminal: GhosttyTerminal,
render_state: GhosttyRenderState,
row_iter: GhosttyRowIterator,
row_cells: GhosttyRowCells,
callback_state: Option<Box<VtCallbackState>>,
cols: u16,
rows: u16,
generation: u64,
force_full_snapshot: bool,
scratch_cols: u16,
scratch_rows: u16,
scratch: Vec<Cell>,
last_cursor: Cursor,
}
unsafe impl Send for VtInner {}
fn default_device_attributes() -> GhosttyDeviceAttributes {
let mut features = [0_u16; 64];
features[0] = GHOSTTY_DA_FEATURE_SELECTIVE_ERASE;
features[1] = GHOSTTY_DA_FEATURE_WINDOWING;
features[2] = GHOSTTY_DA_FEATURE_ANSI_COLOR;
features[3] = GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING;
features[4] = GHOSTTY_DA_FEATURE_CLIPBOARD;
GhosttyDeviceAttributes {
primary: GhosttyDeviceAttributesPrimary {
conformance_level: GHOSTTY_DA_CONFORMANCE_LEVEL_2,
features,
num_features: 5,
},
secondary: GhosttyDeviceAttributesSecondary {
device_type: GHOSTTY_DA_DEVICE_TYPE_VT220,
firmware_version: 0,
rom_cartridge: 0,
},
tertiary: GhosttyDeviceAttributesTertiary { unit_id: 0 },
}
}
impl VtScreen {
pub fn new(cols: u16, rows: u16, theme: Option<&ThemeColors>) -> Result<Self, String> {
Self::new_with_write_pty(cols, rows, theme, None)
}
pub fn new_with_write_pty(
cols: u16,
rows: u16,
theme: Option<&ThemeColors>,
write_pty: Option<PtyWriteCallback>,
) -> Result<Self, String> {
let mut terminal: GhosttyTerminal = std::ptr::null_mut();
let options = GhosttyTerminalOptions {
cols,
rows,
max_scrollback: 10_000,
};
let rc = unsafe { ghostty_terminal_new(std::ptr::null(), &mut terminal, options) };
if rc != 0 || terminal.is_null() {
return Err(format!("ghostty_terminal_new failed: rc={rc}"));
}
let mut callback_state = write_pty.map(|write_pty| {
Box::new(VtCallbackState {
write_pty,
enquiry_response: b"LingXia".to_vec().into_boxed_slice(),
rows: AtomicU16::new(rows),
cols: AtomicU16::new(cols),
cell_width: AtomicU32::new(1),
cell_height: AtomicU32::new(1),
dark_mode: AtomicBool::new(false),
device_attributes: default_device_attributes(),
})
});
if let Some(state) = callback_state.as_mut() {
let userdata = state.as_mut() as *mut VtCallbackState as *mut c_void;
let rc = unsafe {
ghostty_terminal_set(terminal, GhosttyTerminalOption::Userdata, userdata)
};
if rc != 0 {
unsafe { ghostty_terminal_free(terminal) };
return Err(format!("ghostty_terminal_set(USERDATA) failed: rc={rc}"));
}
let rc = unsafe {
ghostty_terminal_set(
terminal,
GhosttyTerminalOption::WritePty,
vt_write_pty_callback as *const c_void,
)
};
if rc != 0 {
unsafe { ghostty_terminal_free(terminal) };
return Err(format!("ghostty_terminal_set(WRITE_PTY) failed: rc={rc}"));
}
let callback_options = [
(
GhosttyTerminalOption::Enquiry,
vt_enquiry_callback as *const c_void,
"ENQUIRY",
),
(
GhosttyTerminalOption::Size,
vt_size_callback as *const c_void,
"SIZE",
),
(
GhosttyTerminalOption::ColorScheme,
vt_color_scheme_callback as *const c_void,
"COLOR_SCHEME",
),
(
GhosttyTerminalOption::DeviceAttributes,
vt_device_attributes_callback as *const c_void,
"DEVICE_ATTRIBUTES",
),
(
GhosttyTerminalOption::Xtversion,
vt_xtversion_callback as *const c_void,
"XTVERSION",
),
];
for (option, callback, label) in callback_options {
let rc = unsafe { ghostty_terminal_set(terminal, option, callback) };
if rc != 0 {
unsafe { ghostty_terminal_free(terminal) };
return Err(format!("ghostty_terminal_set({label}) failed: rc={rc}"));
}
}
}
let mut render_state: GhosttyRenderState = std::ptr::null_mut();
let mut row_iter: GhosttyRowIterator = std::ptr::null_mut();
let mut row_cells: GhosttyRowCells = std::ptr::null_mut();
let enable_render_state = std::env::var("LINGXIA_GHOSTTY_VT_RENDER_STATE")
.map(|s| matches!(s.as_str(), "0" | "false" | "no" | "off"))
.map(|disabled| !disabled)
.unwrap_or(true);
if enable_render_state {
let rc = unsafe { ghostty_render_state_new(std::ptr::null(), &mut render_state) };
if rc != 0 || render_state.is_null() {
unsafe { ghostty_terminal_free(terminal) };
return Err(format!("ghostty_render_state_new failed: rc={rc}"));
}
let rc =
unsafe { ghostty_render_state_row_iterator_new(std::ptr::null(), &mut row_iter) };
if rc != 0 || row_iter.is_null() {
unsafe { ghostty_render_state_free(render_state) };
unsafe { ghostty_terminal_free(terminal) };
return Err(format!(
"ghostty_render_state_row_iterator_new failed: rc={rc}"
));
}
let rc =
unsafe { ghostty_render_state_row_cells_new(std::ptr::null(), &mut row_cells) };
if rc != 0 || row_cells.is_null() {
unsafe { ghostty_render_state_row_iterator_free(row_iter) };
unsafe { ghostty_render_state_free(render_state) };
unsafe { ghostty_terminal_free(terminal) };
return Err(format!(
"ghostty_render_state_row_cells_new failed: rc={rc}"
));
}
} else {
eprintln!(
"VtScreen::new: render_state disabled via \
LINGXIA_GHOSTTY_VT_RENDER_STATE=0 — terminal output will \
parse but cells won't render."
);
}
if let Some(theme) = theme {
unsafe { apply_theme_to_terminal(terminal, theme) };
}
Ok(Self {
inner: Arc::new(Mutex::new(VtInner {
terminal,
render_state,
row_iter,
row_cells,
callback_state,
cols,
rows,
generation: 0,
force_full_snapshot: true,
scratch_cols: cols,
scratch_rows: rows,
scratch: Vec::with_capacity(cols as usize * rows as usize),
last_cursor: Cursor::default(),
})),
})
}
pub fn set_theme(&self, theme: &ThemeColors) {
let mut inner = self.inner.lock();
unsafe { apply_theme_to_terminal(inner.terminal, theme) };
inner.force_full_snapshot = true;
inner.generation = inner.generation.wrapping_add(1);
}
pub fn bump_generation(&self) {
let mut inner = self.inner.lock();
inner.generation = inner.generation.wrapping_add(1);
}
pub fn generation(&self) -> u64 {
self.inner.lock().generation
}
pub fn feed(&self, bytes: &[u8]) {
let mut inner = self.inner.lock();
unsafe { ghostty_terminal_vt_write(inner.terminal, bytes.as_ptr(), bytes.len()) };
inner.generation = inner.generation.wrapping_add(1);
}
pub fn resize(
&self,
cols: u16,
rows: u16,
cell_width_px: u32,
cell_height_px: u32,
) -> Result<(), String> {
let mut inner = self.inner.lock();
let rc = unsafe {
ghostty_terminal_resize(inner.terminal, cols, rows, cell_width_px, cell_height_px)
};
if rc != 0 {
return Err(format!("ghostty_terminal_resize failed: rc={rc}"));
}
inner.cols = cols;
inner.rows = rows;
if let Some(state) = inner.callback_state.as_ref() {
state.cols.store(cols, Ordering::Release);
state.rows.store(rows, Ordering::Release);
state
.cell_width
.store(cell_width_px.max(1), Ordering::Release);
state
.cell_height
.store(cell_height_px.max(1), Ordering::Release);
}
let total = cols as usize * rows as usize;
inner.scratch.clear();
inner.scratch.resize(total, Cell::default());
inner.scratch_cols = cols;
inner.scratch_rows = rows;
inner.force_full_snapshot = true;
inner.generation = inner.generation.wrapping_add(1);
Ok(())
}
pub fn snapshot(&self) -> ScreenSnapshot {
let snapshot_started = perf_trace_enabled().then(Instant::now);
let mut inner = self.inner.lock();
let requested_cols = inner.cols;
let requested_rows = inner.rows;
if inner.render_state.is_null() {
return ScreenSnapshot {
cols: requested_cols,
rows: requested_rows,
cells: Vec::new(),
dirty_rows: Vec::new(),
cursor: Cursor::default(),
default_fg: 0xffffffff,
default_bg: 0x282c34ff,
title: None,
generation: inner.generation,
};
}
let rc = unsafe { ghostty_render_state_update(inner.render_state, inner.terminal) };
if rc != 0 {
eprintln!("ghostty_render_state_update rc={rc}");
}
let mut default_fg = GhosttyColorRgb {
r: 0xCC,
g: 0xCC,
b: 0xCC,
};
let mut default_bg = GhosttyColorRgb::default();
unsafe {
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::ColorForeground,
&mut default_fg as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::ColorBackground,
&mut default_bg as *mut _ as *mut c_void,
);
}
let mut cols = requested_cols;
let mut rows = requested_rows;
unsafe {
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::Cols,
&mut cols as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::Rows,
&mut rows as *mut _ as *mut c_void,
);
}
cols = cols.max(1);
rows = rows.max(1);
let mut force_all_dirty = inner.force_full_snapshot;
let mut full_redraw = force_all_dirty;
let mut state_dirty = GhosttyRenderStateDirty::False;
unsafe {
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::Dirty,
&mut state_dirty as *mut _ as *mut c_void,
);
}
if state_dirty == GhosttyRenderStateDirty::Full {
full_redraw = true;
}
let total = cols as usize * rows as usize;
if inner.scratch.len() != total || inner.scratch_cols != cols || inner.scratch_rows != rows
{
inner.scratch.clear();
inner.scratch.resize(total, Cell::default());
inner.scratch_cols = cols;
inner.scratch_rows = rows;
force_all_dirty = true;
full_redraw = true;
}
let mut dirty_rows: Vec<u16> = Vec::new();
let rc = unsafe {
ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::RowIterator,
&mut inner.row_iter as *mut _ as *mut c_void,
)
};
if rc != 0 {
eprintln!("ghostty_render_state_get(ROW_ITERATOR) rc={rc}");
return empty_snapshot(cols, rows, inner.generation);
}
let mut row_idx: u16 = 0;
while unsafe { ghostty_render_state_row_iterator_next(inner.row_iter) } {
if row_idx >= rows {
break;
}
let mut dirty = GhosttyRenderStateDirty::False;
unsafe {
let _ = ghostty_render_state_row_get(
inner.row_iter,
GhosttyRenderStateRowData::Dirty,
&mut dirty as *mut _ as *mut c_void,
);
}
if !full_redraw && dirty == GhosttyRenderStateDirty::False {
row_idx += 1;
continue;
}
let mut row_changed = force_all_dirty;
let rc = unsafe {
ghostty_render_state_row_get(
inner.row_iter,
GhosttyRenderStateRowData::Cells,
&mut inner.row_cells as *mut _ as *mut c_void,
)
};
if rc != 0 {
eprintln!("row_get(CELLS) rc={rc} at row {row_idx}");
row_idx += 1;
continue;
}
let row_start = row_idx as usize * cols as usize;
let mut col_idx: u16 = 0;
while unsafe { ghostty_render_state_row_cells_next(inner.row_cells) } {
if col_idx >= cols {
break;
}
let cell = read_cell(inner.row_cells, default_fg, default_bg);
let idx = row_start + col_idx as usize;
row_changed |= inner.scratch[idx] != cell;
inner.scratch[idx] = cell;
col_idx += 1;
}
for c in col_idx..cols {
let idx = row_start + c as usize;
row_changed |= inner.scratch[idx] != Cell::default();
inner.scratch[idx] = Cell::default();
}
if row_changed {
dirty_rows.push(row_idx);
}
row_idx += 1;
}
if full_redraw && row_idx < rows {
eprintln!(
"vt snapshot full redraw ended early: iter_rows={row_idx} expected_rows={rows} cols={cols}"
);
for trailing_row in row_idx..rows {
let row_start = trailing_row as usize * cols as usize;
let row_end = row_start + cols as usize;
let mut row_changed = force_all_dirty;
for cell in &mut inner.scratch[row_start..row_end] {
row_changed |= *cell != Cell::default();
*cell = Cell::default();
}
if row_changed {
dirty_rows.push(trailing_row);
}
}
}
inner.force_full_snapshot = false;
let mut visible: bool = false;
let mut col_u16: u16 = 0;
let mut row_u16: u16 = 0;
let mut cursor_style = GhosttyRenderStateCursorVisualStyle::Block;
unsafe {
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::CursorViewportX,
&mut col_u16 as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::CursorViewportY,
&mut row_u16 as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::CursorVisible,
&mut visible as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_get(
inner.render_state,
GhosttyRenderStateData::CursorVisualStyle,
&mut cursor_style as *mut _ as *mut c_void,
);
}
let cursor = Cursor {
col: col_u16,
row: row_u16,
visible,
style: cursor_style,
};
let previous_cursor = inner.last_cursor;
if previous_cursor != cursor {
if previous_cursor.visible && previous_cursor.row < rows {
push_unique_row(&mut dirty_rows, previous_cursor.row);
}
if cursor.visible && cursor.row < rows {
push_unique_row(&mut dirty_rows, cursor.row);
}
inner.last_cursor = cursor;
}
dirty_rows.sort_unstable();
let clone_started = perf_trace_enabled().then(Instant::now);
let cells = inner.scratch.clone();
let clone_elapsed_ms =
clone_started.map(|started| started.elapsed().as_secs_f64() * 1000.0);
let title = terminal_title(&inner);
let snapshot = ScreenSnapshot {
cols,
rows,
cells,
dirty_rows,
cursor,
default_fg: pack_rgb(default_fg, 0xFF),
default_bg: pack_rgb(default_bg, 0xFF),
title,
generation: inner.generation,
};
if let Some(started) = snapshot_started {
let total_ms = started.elapsed().as_secs_f64() * 1000.0;
static LAST_LOGGED_GENERATION: AtomicU64 = AtomicU64::new(u64::MAX);
let previous = LAST_LOGGED_GENERATION.load(Ordering::Relaxed);
let generation_changed = previous != snapshot.generation
&& LAST_LOGGED_GENERATION
.compare_exchange(
previous,
snapshot.generation,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok();
let should_log = perf_trace_verbose() || generation_changed || total_ms >= 2.0;
if should_log {
eprintln!(
"vt_snapshot rows={} cols={} dirty_rows={} full_redraw={} cells={} clone_ms={:.3} total_ms={:.3}",
rows,
cols,
snapshot.dirty_rows.len(),
full_redraw,
snapshot.cells.len(),
clone_elapsed_ms.unwrap_or_default(),
total_ms
);
}
}
snapshot
}
pub fn size(&self) -> (u16, u16) {
let inner = self.inner.lock();
(inner.cols, inner.rows)
}
pub fn set_dark_mode(&self, dark: bool) {
let inner = self.inner.lock();
if let Some(state) = inner.callback_state.as_ref() {
state.dark_mode.store(dark, Ordering::Release);
}
}
pub fn mouse_tracking_active(&self) -> bool {
self.mode_active(MODE_NORMAL_MOUSE)
|| self.mode_active(MODE_BUTTON_MOUSE)
|| self.mode_active(MODE_ANY_MOUSE)
|| self.mode_active(MODE_X10_MOUSE)
}
pub fn is_sgr_mouse(&self) -> bool {
self.mode_active(MODE_SGR_MOUSE)
}
pub fn is_alt_scroll(&self) -> bool {
self.mode_active(MODE_ALT_SCROLL)
}
pub fn scrollbar(&self) -> Option<GhosttyScrollbar> {
let inner = self.inner.lock();
if inner.terminal.is_null() {
return None;
}
let mut scrollbar = GhosttyTerminalScrollbar::default();
let rc = unsafe {
ghostty_terminal_get(
inner.terminal,
GhosttyTerminalData::Scrollbar,
&mut scrollbar as *mut _ as *mut c_void,
)
};
(rc == 0).then_some(GhosttyScrollbar {
total: scrollbar.total,
offset: scrollbar.offset,
len: scrollbar.len,
})
}
pub fn is_alternate_screen(&self) -> bool {
let inner = self.inner.lock();
if inner.terminal.is_null() {
return false;
}
let mut screen = GhosttyTerminalScreen::Primary;
let rc = unsafe {
ghostty_terminal_get(
inner.terminal,
GhosttyTerminalData::ActiveScreen,
&mut screen as *mut _ as *mut c_void,
)
};
rc == 0 && screen == GhosttyTerminalScreen::Alternate
}
pub fn scroll_viewport_delta(&self, delta_rows: isize) -> bool {
if delta_rows == 0 {
return false;
}
let mut inner = self.inner.lock();
if inner.terminal.is_null() {
return false;
}
let behavior = GhosttyTerminalScrollViewport {
tag: GhosttyTerminalScrollViewportTag::Delta,
value: GhosttyTerminalScrollViewportValue { delta: delta_rows },
};
unsafe { ghostty_terminal_scroll_viewport(inner.terminal, behavior) };
inner.force_full_snapshot = true;
inner.generation = inner.generation.wrapping_add(1);
true
}
pub fn is_bracketed_paste(&self) -> bool {
self.mode_active(MODE_BRACKETED_PASTE)
}
pub fn is_decckm(&self) -> bool {
self.mode_active(MODE_DECCKM)
}
pub fn mode_active(&self, mode: GhosttyMode) -> bool {
let inner = self.inner.lock();
if inner.terminal.is_null() {
return false;
}
let mut on: bool = false;
let rc = unsafe { ghostty_terminal_mode_get(inner.terminal, mode, &mut on) };
rc == 0 && on
}
}
unsafe extern "C" fn vt_write_pty_callback(
_terminal: GhosttyTerminal,
userdata: *mut c_void,
data: *const u8,
len: usize,
) {
if userdata.is_null() || data.is_null() || len == 0 {
return;
}
let state = unsafe { &*(userdata as *const VtCallbackState) };
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
(state.write_pty)(bytes);
}
unsafe extern "C" fn vt_enquiry_callback(
_terminal: GhosttyTerminal,
userdata: *mut c_void,
) -> GhosttyString {
if userdata.is_null() {
return GhosttyString {
ptr: std::ptr::null(),
len: 0,
};
}
let state = unsafe { &*(userdata as *const VtCallbackState) };
GhosttyString {
ptr: state.enquiry_response.as_ptr(),
len: state.enquiry_response.len(),
}
}
unsafe extern "C" fn vt_size_callback(
_terminal: GhosttyTerminal,
userdata: *mut c_void,
out_size: *mut GhosttySizeReportSize,
) -> bool {
if userdata.is_null() || out_size.is_null() {
return false;
}
let state = unsafe { &*(userdata as *const VtCallbackState) };
unsafe {
*out_size = GhosttySizeReportSize {
rows: state.rows.load(Ordering::Acquire).max(1),
columns: state.cols.load(Ordering::Acquire).max(1),
cell_width: state.cell_width.load(Ordering::Acquire).max(1),
cell_height: state.cell_height.load(Ordering::Acquire).max(1),
};
}
true
}
unsafe extern "C" fn vt_color_scheme_callback(
_terminal: GhosttyTerminal,
userdata: *mut c_void,
out_scheme: *mut GhosttyColorScheme,
) -> bool {
if userdata.is_null() || out_scheme.is_null() {
return false;
}
let state = unsafe { &*(userdata as *const VtCallbackState) };
unsafe {
*out_scheme = if state.dark_mode.load(Ordering::Acquire) {
GhosttyColorScheme::Dark
} else {
GhosttyColorScheme::Light
};
}
true
}
unsafe extern "C" fn vt_device_attributes_callback(
_terminal: GhosttyTerminal,
userdata: *mut c_void,
out_attrs: *mut GhosttyDeviceAttributes,
) -> bool {
if userdata.is_null() || out_attrs.is_null() {
return false;
}
let state = unsafe { &*(userdata as *const VtCallbackState) };
unsafe {
*out_attrs = state.device_attributes;
}
true
}
unsafe extern "C" fn vt_xtversion_callback(
_terminal: GhosttyTerminal,
_userdata: *mut c_void,
) -> GhosttyString {
GhosttyString {
ptr: std::ptr::null(),
len: 0,
}
}
fn empty_snapshot(cols: u16, rows: u16, generation: u64) -> ScreenSnapshot {
ScreenSnapshot {
cols,
rows,
cells: Vec::new(),
dirty_rows: Vec::new(),
cursor: Cursor::default(),
default_fg: 0xffffffff,
default_bg: 0x282c34ff,
title: None,
generation,
}
}
fn pack_rgb(c: GhosttyColorRgb, a: u8) -> u32 {
((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | (a as u32)
}
fn push_unique_row(rows: &mut Vec<u16>, row: u16) {
if !rows.contains(&row) {
rows.push(row);
}
}
fn terminal_title(inner: &VtInner) -> Option<String> {
if inner.terminal.is_null() {
return None;
}
let mut title = GhosttyString::default();
let rc = unsafe {
ghostty_terminal_get(
inner.terminal,
GhosttyTerminalData::Title,
&mut title as *mut _ as *mut c_void,
)
};
if rc != 0 || title.ptr.is_null() || title.len == 0 {
return None;
}
let bytes = unsafe { std::slice::from_raw_parts(title.ptr, title.len) };
std::str::from_utf8(bytes)
.ok()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn read_cell(
cells: GhosttyRowCells,
default_fg: GhosttyColorRgb,
default_bg: GhosttyColorRgb,
) -> Cell {
let mut raw: GhosttyCell = 0;
let mut style = GhosttyStyle::new();
let mut bg = GhosttyColorRgb::default();
let mut fg = GhosttyColorRgb::default();
unsafe {
let _ = ghostty_render_state_row_cells_get(
cells,
GhosttyRenderStateRowCellsData::Raw,
&mut raw as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_row_cells_get(
cells,
GhosttyRenderStateRowCellsData::Style,
&mut style as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_row_cells_get(
cells,
GhosttyRenderStateRowCellsData::BgColor,
&mut bg as *mut _ as *mut c_void,
);
let _ = ghostty_render_state_row_cells_get(
cells,
GhosttyRenderStateRowCellsData::FgColor,
&mut fg as *mut _ as *mut c_void,
);
}
let mut has_text: bool = false;
let mut wide_mode = GhosttyCellWide::Narrow;
unsafe {
let _ = ghostty_cell_get(
raw,
GhosttyCellData::HasText,
&mut has_text as *mut _ as *mut c_void,
);
let _ = ghostty_cell_get(
raw,
GhosttyCellData::Wide,
&mut wide_mode as *mut _ as *mut c_void,
);
}
let is_spacer = matches!(
wide_mode,
GhosttyCellWide::SpacerTail | GhosttyCellWide::SpacerHead
);
let text = if has_text && !is_spacer {
read_cell_text(cells, raw)
} else {
String::new()
};
let wide = wide_mode == GhosttyCellWide::Wide;
const STYLE_COLOR_TAG_NONE: u32 = 0;
let fg_was_default = style.fg_color.tag == STYLE_COLOR_TAG_NONE;
let bg_was_default = style.bg_color.tag == STYLE_COLOR_TAG_NONE;
let fg = if fg_was_default { default_fg } else { fg };
let bg = if bg_was_default { default_bg } else { bg };
let bg_alpha: u8 = if bg_was_default { 0 } else { 0xFF };
let mut attrs: u8 = 0;
if style.bold {
attrs |= ATTR_BOLD;
}
if style.italic {
attrs |= ATTR_ITALIC;
}
if style.underline != 0 {
attrs |= ATTR_UNDERLINE;
}
if style.strikethrough {
attrs |= ATTR_STRIKE;
}
if style.inverse {
attrs |= ATTR_INVERSE;
}
Cell {
text,
fg: pack_rgb(fg, 0xFF),
bg: pack_rgb(bg, bg_alpha),
attrs,
wide,
_pad: [0; 2],
}
}
fn read_cell_text(cells: GhosttyRowCells, raw: GhosttyCell) -> String {
let mut graphemes_len: u32 = 0;
unsafe {
let _ = ghostty_render_state_row_cells_get(
cells,
GhosttyRenderStateRowCellsData::GraphemesLen,
&mut graphemes_len as *mut _ as *mut c_void,
);
}
if graphemes_len > 0 && graphemes_len <= 64 {
let mut codepoints = vec![0u32; graphemes_len as usize];
unsafe {
let _ = ghostty_render_state_row_cells_get(
cells,
GhosttyRenderStateRowCellsData::GraphemesBuf,
codepoints.as_mut_ptr() as *mut c_void,
);
}
let mut text = String::with_capacity(codepoints.len());
for codepoint in codepoints {
if let Some(ch) = char::from_u32(codepoint) {
text.push(ch);
}
}
return text;
}
let mut codepoint: u32 = 0;
unsafe {
let _ = ghostty_cell_get(
raw,
GhosttyCellData::Codepoint,
&mut codepoint as *mut _ as *mut c_void,
);
}
char::from_u32(codepoint)
.filter(|ch| *ch != '\0')
.map(String::from)
.unwrap_or_default()
}
impl Drop for VtScreen {
fn drop(&mut self) {
if let Some(mutex) = Arc::get_mut(&mut self.inner) {
let inner = mutex.get_mut();
if !inner.row_cells.is_null() {
unsafe { ghostty_render_state_row_cells_free(inner.row_cells) };
inner.row_cells = std::ptr::null_mut();
}
if !inner.row_iter.is_null() {
unsafe { ghostty_render_state_row_iterator_free(inner.row_iter) };
inner.row_iter = std::ptr::null_mut();
}
if !inner.render_state.is_null() {
unsafe { ghostty_render_state_free(inner.render_state) };
inner.render_state = std::ptr::null_mut();
}
if !inner.terminal.is_null() {
unsafe { ghostty_terminal_free(inner.terminal) };
inner.terminal = std::ptr::null_mut();
}
}
}
}