#![allow(dead_code)]

#[allow(
    dead_code,
    non_camel_case_types,
    non_snake_case,
    non_upper_case_globals,
    clippy::all,
    rustdoc::all
)]
pub mod bindings;

use std::cell::Cell;
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::ffi::c_void;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::mem;
use std::ops::RangeInclusive;
use std::os::raw::c_char;
use std::ptr;
use std::slice;
use std::sync::{Mutex, Once, OnceLock};

pub use bindings as ffi;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Error(ffi::GhosttyResult);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FormatterFormat {
    Plain,
    Vt,
}

impl FormatterFormat {
    fn as_raw(self) -> ffi::GhosttyFormatterFormat {
        match self {
            Self::Plain => ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_PLAIN,
            Self::Vt => ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_VT,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ghostty error {}", self.0)
    }
}

impl std::error::Error for Error {}

trait GhosttyResultExt {
    fn into_result(self) -> Result<(), Error>;
}

impl GhosttyResultExt for ffi::GhosttyResult {
    fn into_result(self) -> Result<(), Error> {
        if self == ffi::GhosttyResult_GHOSTTY_SUCCESS {
            Ok(())
        } else {
            Err(Error(self))
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dirty {
    Clean,
    Partial,
    Full,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RowSelection {
    pub start_x: u16,
    pub end_x: u16,
}

impl RowSelection {
    pub fn range(self) -> RangeInclusive<u16> {
        self.start_x..=self.end_x
    }
}

impl Dirty {
    fn from_raw(value: ffi::GhosttyRenderStateDirty) -> Self {
        match value {
            ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE => Self::Clean,
            ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_PARTIAL => Self::Partial,
            ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FULL => Self::Full,
            _ => Self::Full,
        }
    }

    fn as_raw(self) -> ffi::GhosttyRenderStateDirty {
        match self {
            Self::Clean => ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE,
            Self::Partial => ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_PARTIAL,
            Self::Full => ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FULL,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusEvent {
    Gained,
    Lost,
}

impl FocusEvent {
    fn as_raw(self) -> ffi::GhosttyFocusEvent {
        match self {
            Self::Gained => ffi::GhosttyFocusEvent_GHOSTTY_FOCUS_GAINED,
            Self::Lost => ffi::GhosttyFocusEvent_GHOSTTY_FOCUS_LOST,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorScheme {
    Light,
    Dark,
}

impl ColorScheme {
    fn as_raw(self) -> ffi::GhosttyColorScheme {
        match self {
            Self::Light => ffi::GhosttyColorScheme_GHOSTTY_COLOR_SCHEME_LIGHT,
            Self::Dark => ffi::GhosttyColorScheme_GHOSTTY_COLOR_SCHEME_DARK,
        }
    }
}

pub const MOD_SHIFT: u16 = ffi::GHOSTTY_MODS_SHIFT as u16;
pub const MOD_CTRL: u16 = ffi::GHOSTTY_MODS_CTRL as u16;
pub const MOD_ALT: u16 = ffi::GHOSTTY_MODS_ALT as u16;
pub const MOD_SUPER: u16 = ffi::GHOSTTY_MODS_SUPER as u16;

pub const KEY_ENTER: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ENTER;
pub const KEY_UP: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_UP;
pub const KEY_DOWN: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_DOWN;
pub const KEY_LEFT: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_LEFT;
pub const KEY_RIGHT: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_RIGHT;
pub const KEY_A: u32 = ffi::GhosttyKey_GHOSTTY_KEY_A;

pub const MOUSE_ACTION_PRESS: ffi::GhosttyMouseAction =
    ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_PRESS;
pub const MOUSE_ACTION_RELEASE: ffi::GhosttyMouseAction =
    ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_RELEASE;
pub const MOUSE_ACTION_MOTION: ffi::GhosttyMouseAction =
    ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_MOTION;
pub const MOUSE_BUTTON_LEFT: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_LEFT;
pub const MOUSE_BUTTON_RIGHT: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_RIGHT;
pub const MOUSE_BUTTON_MIDDLE: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_MIDDLE;
pub const MOUSE_BUTTON_WHEEL_UP: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_FOUR;
pub const MOUSE_BUTTON_WHEEL_DOWN: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_FIVE;
pub const MOUSE_BUTTON_WHEEL_LEFT: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SIX;
pub const MOUSE_BUTTON_WHEEL_RIGHT: ffi::GhosttyMouseButton =
    ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SEVEN;
pub const MOUSE_FORMAT_SGR: ffi::GhosttyMouseFormat =
    ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_SGR;

pub const MODE_APPLICATION_CURSOR_KEYS: u16 = 1;
pub const MODE_FOCUS_EVENT: u16 = 1004;
pub const MODE_MOUSE_UTF8: u16 = 1005;
pub const MODE_MOUSE_SGR: u16 = 1006;
pub const MODE_MOUSE_ALTERNATE_SCROLL: u16 = 1007;
pub const MODE_MOUSE_SGR_PIXELS: u16 = 1016;
pub const MODE_BRACKETED_PASTE: u16 = 2004;
pub const MODE_SYNCHRONIZED_OUTPUT: u16 = 2026;
pub const MODE_GRAPHEME_CLUSTER: u16 = 2027;
pub const MODE_COLOR_SCHEME_REPORT: u16 = 2031;
// These are documented in vendor/libghostty-vt/include/ghostty/vt/terminal.h,
// but the generated bindings do not currently expose named constants for them.
const TERMINAL_DATA_COLOR_FOREGROUND: ffi::GhosttyTerminalData = 18;
const TERMINAL_DATA_COLOR_CURSOR: ffi::GhosttyTerminalData = 20;

const KITTY_IMAGE_STORAGE_LIMIT_BYTES: u64 = 64 * 1024 * 1024;
const APC_MAX_BYTES: usize = 16 * 1024 * 1024;
const APC_MAX_BYTES_KITTY: usize = 16 * 1024 * 1024;
pub(crate) const KITTY_UNICODE_PLACEHOLDER: u32 = 0x10EEEE;
// The vendored C headers expose these placement fields, but the checked-in
// generated bindings predate the names. Keep the explicit values aligned with
// vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h.
const KITTY_PLACEMENT_DATA_IS_VIRTUAL: ffi::GhosttyKittyGraphicsPlacementData = 3;
const KITTY_PLACEMENT_DATA_COLUMNS: ffi::GhosttyKittyGraphicsPlacementData = 10;
const KITTY_PLACEMENT_DATA_ROWS: ffi::GhosttyKittyGraphicsPlacementData = 11;

static INSTALL_PNG_DECODER: Once = Once::new();
static KITTY_PLACEHOLDER_DIACRITICS: OnceLock<HashMap<u32, u32>> = OnceLock::new();

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum KittyImageFormat {
    Rgb,
    Rgba,
    Png,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KittyImagePlacement {
    pub image_id: u32,
    pub placement_id: u32,
    pub z: i32,
    pub x_offset: u32,
    pub y_offset: u32,
    pub image_width: u32,
    pub image_height: u32,
    pub format: KittyImageFormat,
    pub data_len: usize,
    pub data_fingerprint: u64,
    pub data: Vec<u8>,
    pub render: KittyPlacementRenderInfo,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KittyImageDescriptor {
    pub image_id: u32,
    pub placement_id: u32,
    pub image_width: u32,
    pub image_height: u32,
    pub format: KittyImageFormat,
    pub data_len: usize,
    pub data_fingerprint: u64,
}

#[derive(Debug, Clone, Copy)]
struct KittyImageFingerprintEntry {
    generation: u64,
    fingerprint: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KittyPlacementRenderInfo {
    pub pixel_width: u32,
    pub pixel_height: u32,
    pub grid_cols: u32,
    pub grid_rows: u32,
    pub viewport_col: i32,
    pub viewport_row: i32,
    pub source_x: u32,
    pub source_y: u32,
    pub source_width: u32,
    pub source_height: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KittyVirtualPlacementSpec {
    image_id: u32,
    placement_id: u32,
    columns: u32,
    rows: u32,
    z: i32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KittyVirtualCell {
    x: u16,
    y: u16,
    image_id_low: u32,
    image_id_high: Option<u32>,
    placement_id: Option<u32>,
    row: Option<u32>,
    col: Option<u32>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KittyVirtualRun {
    x: u16,
    y: u16,
    image_id_low: u32,
    image_id_high: Option<u32>,
    placement_id: Option<u32>,
    row: u32,
    col: u32,
    width: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KittyVirtualPlacementGeometry {
    x_offset: u32,
    y_offset: u32,
    render: KittyPlacementRenderInfo,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorVisualStyle {
    Bar,
    Block,
    Underline,
    BlockHollow,
}

impl CursorVisualStyle {
    fn from_raw(value: ffi::GhosttyRenderStateCursorVisualStyle) -> Self {
        match value {
            ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK => {
                Self::Block
            }
            ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE => {
                Self::Underline
            }
            ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW => {
                Self::BlockHollow
            }
            _ => Self::Bar,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActiveScreen {
    Primary,
    Alternate,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalScrollbar {
    pub total: usize,
    pub offset: usize,
    pub len: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorViewport {
    pub x: u16,
    pub y: u16,
    pub wide_tail: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RgbColor {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

impl From<ffi::GhosttyColorRgb> for RgbColor {
    fn from(value: ffi::GhosttyColorRgb) -> Self {
        Self {
            r: value.r,
            g: value.g,
            b: value.b,
        }
    }
}

pub fn default_palette() -> [RgbColor; 256] {
    let mut palette = [ffi::GhosttyColorRgb::default(); 256];
    unsafe { ffi::ghostty_color_palette_default(palette.as_mut_ptr()) };
    palette.map(Into::into)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellColor {
    Palette(u8),
    Rgb(RgbColor),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellStyle {
    pub fg_color: Option<CellColor>,
    pub bg_color: Option<CellColor>,
    pub underline_color: Option<CellColor>,
    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: u8,
    pub underlined: bool,
}

impl From<ffi::GhosttyStyle> for CellStyle {
    fn from(value: ffi::GhosttyStyle) -> Self {
        Self {
            fg_color: cell_color_from_style_color(value.fg_color),
            bg_color: cell_color_from_style_color(value.bg_color),
            underline_color: cell_color_from_style_color(value.underline_color),
            bold: value.bold,
            italic: value.italic,
            faint: value.faint,
            blink: value.blink,
            inverse: value.inverse,
            invisible: value.invisible,
            strikethrough: value.strikethrough,
            overline: value.overline,
            underline: normalize_underline_style(value.underline),
            underlined: value.underline != 0,
        }
    }
}

fn normalize_underline_style(value: std::os::raw::c_int) -> u8 {
    match value {
        0..=5 => value as u8,
        _ => 1,
    }
}

fn cell_color_from_style_color(color: ffi::GhosttyStyleColor) -> Option<CellColor> {
    match color.tag {
        ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_PALETTE => {
            // SAFETY: Ghostty's tagged union stores `palette` when the tag is PALETTE.
            Some(CellColor::Palette(unsafe { color.value.palette }))
        }
        ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_RGB => {
            // SAFETY: Ghostty's tagged union stores `rgb` when the tag is RGB.
            Some(CellColor::Rgb(unsafe { color.value.rgb }.into()))
        }
        _ => None,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RenderColors {
    pub background: RgbColor,
    pub foreground: RgbColor,
    pub palette: [RgbColor; 256],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellWide {
    Narrow,
    Wide,
    SpacerTail,
    SpacerHead,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ScreenTextCell {
    pub wide: CellWide,
    pub graphemes: Vec<u32>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ScreenTextRow {
    pub cells: Vec<ScreenTextCell>,
    pub soft_wrapped: bool,
    pub wrap_continuation: bool,
}

impl CellWide {
    fn from_raw(value: ffi::GhosttyCellWide) -> Self {
        match value {
            ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW => Self::Narrow,
            ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_WIDE => Self::Wide,
            ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_TAIL => Self::SpacerTail,
            ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_HEAD => Self::SpacerHead,
            _ => Self::Narrow,
        }
    }
}

type WritePtyCallback = dyn FnMut(&[u8]) + Send;

const MAX_CLIPBOARD_BYTES: usize = 192 * 1024;

#[derive(Default)]
struct TerminalCallbackState {
    write_pty: Option<Box<WritePtyCallback>>,
    bell_count: u16,
    pwd_changes: Vec<Vec<u8>>,
    clipboard_writes: Vec<Vec<u8>>,
    size_report: ffi::GhosttySizeReportSize,
    color_scheme: Option<ColorScheme>,
}

unsafe extern "C" fn bell_trampoline(_terminal: ffi::GhosttyTerminal, userdata: *mut c_void) {
    if userdata.is_null() {
        return;
    }
    // SAFETY: userdata is the TerminalCallbackState installed with this terminal.
    let state = unsafe { &mut *userdata.cast::<TerminalCallbackState>() };
    state.bell_count = state.bell_count.saturating_add(1);
}

unsafe extern "C" fn color_scheme_trampoline(
    _terminal: ffi::GhosttyTerminal,
    userdata: *mut c_void,
    out_scheme: *mut ffi::GhosttyColorScheme,
) -> bool {
    if userdata.is_null() || out_scheme.is_null() {
        return false;
    }
    let state = unsafe { &*userdata.cast::<TerminalCallbackState>() };
    let Some(color_scheme) = state.color_scheme else {
        return false;
    };
    unsafe {
        out_scheme.write(color_scheme.as_raw());
    }
    true
}

unsafe extern "C" fn size_trampoline(
    _terminal: ffi::GhosttyTerminal,
    userdata: *mut c_void,
    out_size: *mut ffi::GhosttySizeReportSize,
) -> bool {
    if userdata.is_null() || out_size.is_null() {
        return false;
    }
    let state = unsafe { &*userdata.cast::<TerminalCallbackState>() };
    let size = state.size_report;
    if size.rows == 0 || size.columns == 0 || size.cell_width == 0 || size.cell_height == 0 {
        return false;
    }
    unsafe {
        out_size.write(size);
    }
    true
}

unsafe extern "C" fn write_pty_trampoline(
    _terminal: ffi::GhosttyTerminal,
    userdata: *mut c_void,
    data: *const u8,
    len: usize,
) {
    if userdata.is_null() || (data.is_null() && len != 0) {
        return;
    }
    let state = unsafe { &mut *(userdata.cast::<TerminalCallbackState>()) };
    let Some(callback) = state.write_pty.as_mut() else {
        return;
    };
    let bytes = if len == 0 {
        &[]
    } else {
        unsafe { slice::from_raw_parts(data, len) }
    };
    callback(bytes);
}

unsafe extern "C" fn clipboard_write_trampoline(
    _terminal: ffi::GhosttyTerminal,
    userdata: *mut c_void,
    write: *const ffi::GhosttyClipboardWrite,
) -> ffi::GhosttyClipboardWriteResult {
    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        // SAFETY: libghostty-vt owns these values for the synchronous callback.
        unsafe { capture_clipboard_write(userdata, write) }
    }))
    .unwrap_or(ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA)
}

unsafe fn capture_clipboard_write(
    userdata: *mut c_void,
    write: *const ffi::GhosttyClipboardWrite,
) -> ffi::GhosttyClipboardWriteResult {
    if userdata.is_null() || write.is_null() {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
    }

    let required_size = std::mem::offset_of!(ffi::GhosttyClipboardWrite, contents_len)
        + std::mem::size_of::<usize>();
    // SAFETY: size is the leading field of the live request.
    if unsafe { (*write).size } < required_size {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
    }
    // SAFETY: the size check covers every field accessed below.
    let request = unsafe { &*write };
    if request.location != ffi::GhosttyClipboardLocation_GHOSTTY_CLIPBOARD_LOCATION_STANDARD {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
    }

    // SAFETY: userdata is the TerminalCallbackState installed with this terminal.
    let state = unsafe { &mut *userdata.cast::<TerminalCallbackState>() };
    if request.contents_len == 0 {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS;
    }
    if request.contents_len != 1 {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
    }
    if request.contents.is_null() {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
    }

    // SAFETY: libghostty-vt keeps the single content and its strings alive for the callback.
    let content = unsafe { &*request.contents };
    // SAFETY: the MIME string is borrowed from the live callback request.
    let Some(mime) = (unsafe { borrowed_bytes(content.mime) }) else {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
    };
    let is_text = std::str::from_utf8(mime)
        .ok()
        .and_then(|mime| mime.split(';').next())
        .is_some_and(|mime| mime.trim().eq_ignore_ascii_case("text/plain"));
    if !is_text {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
    }

    // SAFETY: the data string is borrowed from the live callback request.
    let Some(bytes) = (unsafe { borrowed_bytes(content.data) }) else {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
    };
    if bytes.is_empty() {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
    }
    if bytes.len() > MAX_CLIPBOARD_BYTES {
        return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
    }
    state.clipboard_writes.push(bytes.to_vec());
    ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS
}

unsafe fn borrowed_bytes<'a>(value: ffi::GhosttyString) -> Option<&'a [u8]> {
    if value.len == 0 {
        Some(&[])
    } else if value.ptr.is_null() {
        None
    } else {
        // SAFETY: the callback contract keeps pointer and length valid until return.
        Some(unsafe { slice::from_raw_parts(value.ptr, value.len) })
    }
}

unsafe extern "C" fn pwd_changed_trampoline(terminal: ffi::GhosttyTerminal, userdata: *mut c_void) {
    if terminal.is_null() || userdata.is_null() {
        return;
    }
    let mut pwd = ffi::GhosttyString::default();
    let result = unsafe {
        ffi::ghostty_terminal_get(
            terminal,
            ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_PWD,
            (&mut pwd as *mut ffi::GhosttyString).cast(),
        )
    };
    if result != ffi::GhosttyResult_GHOSTTY_SUCCESS || (pwd.ptr.is_null() && pwd.len != 0) {
        return;
    }
    let bytes = if pwd.len == 0 {
        Vec::new()
    } else {
        unsafe { slice::from_raw_parts(pwd.ptr, pwd.len) }.to_vec()
    };
    let state = unsafe { &mut *(userdata.cast::<TerminalCallbackState>()) };
    state.pwd_changes.push(bytes);
}

fn install_png_decoder_once() {
    INSTALL_PNG_DECODER.call_once(|| unsafe {
        let _ = ffi::ghostty_sys_set(
            ffi::GhosttySysOption_GHOSTTY_SYS_OPT_DECODE_PNG,
            (decode_png_trampoline as *const ()).cast(),
        );
    });
}

unsafe extern "C" fn decode_png_trampoline(
    _userdata: *mut c_void,
    allocator: *const ffi::GhosttyAllocator,
    data: *const u8,
    data_len: usize,
    out: *mut ffi::GhosttySysImage,
) -> bool {
    if data.is_null() || out.is_null() {
        return false;
    }
    let bytes = unsafe { slice::from_raw_parts(data, data_len) };
    let Some(rgba) = decode_png_rgba(bytes) else {
        return false;
    };
    let ptr = unsafe { ffi::ghostty_alloc(allocator, rgba.data.len()) };
    if ptr.is_null() {
        return false;
    }
    unsafe {
        ptr::copy_nonoverlapping(rgba.data.as_ptr(), ptr, rgba.data.len());
        *out = ffi::GhosttySysImage {
            width: rgba.width,
            height: rgba.height,
            data: ptr,
            data_len: rgba.data.len(),
        };
    }
    true
}

struct DecodedPng {
    width: u32,
    height: u32,
    data: Vec<u8>,
}

fn decode_png_rgba(bytes: &[u8]) -> Option<DecodedPng> {
    let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
    decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
    let mut reader = decoder.read_info().ok()?;
    let mut buf = vec![0; reader.output_buffer_size()];
    let info = reader.next_frame(&mut buf).ok()?;
    let frame = &buf[..info.buffer_size()];
    if info.bit_depth != png::BitDepth::Eight {
        return None;
    }

    let data = match info.color_type {
        png::ColorType::Rgba => frame.to_vec(),
        png::ColorType::Rgb => {
            let mut out = Vec::with_capacity((info.width as usize) * (info.height as usize) * 4);
            for rgb in frame.chunks_exact(3) {
                out.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
            }
            out
        }
        png::ColorType::Grayscale => {
            let mut out = Vec::with_capacity((info.width as usize) * (info.height as usize) * 4);
            for gray in frame {
                out.extend_from_slice(&[*gray, *gray, *gray, 255]);
            }
            out
        }
        png::ColorType::GrayscaleAlpha => {
            let mut out = Vec::with_capacity((info.width as usize) * (info.height as usize) * 4);
            for ga in frame.chunks_exact(2) {
                out.extend_from_slice(&[ga[0], ga[0], ga[0], ga[1]]);
            }
            out
        }
        png::ColorType::Indexed => return None,
    };

    Some(DecodedPng {
        width: info.width,
        height: info.height,
        data,
    })
}

pub fn unicode_codepoint_width(codepoint: u32) -> u8 {
    unsafe { ffi::ghostty_unicode_codepoint_width(codepoint) }
}

pub fn unicode_grapheme_width(codepoints: &[u32]) -> (usize, u8) {
    let mut width = 0u8;
    let consumed = unsafe {
        ffi::ghostty_unicode_grapheme_width(codepoints.as_ptr(), codepoints.len(), &mut width)
    };
    (consumed, width)
}

pub fn encode_focus(event: FocusEvent) -> Result<Vec<u8>, Error> {
    let mut required = 0usize;
    // SAFETY: null buffer + out len is the documented way to query required size.
    let result =
        unsafe { ffi::ghostty_focus_encode(event.as_raw(), ptr::null_mut(), 0, &mut required) };
    if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
        result.into_result()?;
    }

    let mut buffer = vec![0u8; required];
    // SAFETY: buffer is allocated for required size; function writes at most that many bytes.
    unsafe {
        ffi::ghostty_focus_encode(
            event.as_raw(),
            buffer.as_mut_ptr().cast(),
            buffer.len(),
            &mut required,
        )
        .into_result()?;
    }
    buffer.truncate(required);
    Ok(buffer)
}

pub struct Terminal {
    raw: ffi::GhosttyTerminal,
    max_scrollback: usize,
    #[cfg(windows)]
    tracked_row: ffi::GhosttyTrackedGridRef,
    callback_state: Box<TerminalCallbackState>,
    kitty_fingerprints: Mutex<HashMap<u32, KittyImageFingerprintEntry>>,
    kitty_empty_generation: Cell<Option<u64>>,
}

impl Terminal {
    pub fn new(cols: u16, rows: u16, max_scrollback: usize) -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        let options = ffi::GhosttyTerminalOptions {
            cols,
            rows,
            max_scrollback,
        };
        // SAFETY: valid out pointer and options, null allocator means default allocator.
        unsafe {
            ffi::ghostty_terminal_new(ptr::null(), &mut raw, options).into_result()?;
        }

        let mut terminal = Self {
            raw,
            max_scrollback,
            #[cfg(windows)]
            tracked_row: ptr::null_mut(),
            callback_state: Box::new(TerminalCallbackState {
                size_report: ffi::GhosttySizeReportSize {
                    rows,
                    columns: cols,
                    ..Default::default()
                },
                ..Default::default()
            }),
            kitty_fingerprints: Mutex::new(HashMap::new()),
            kitty_empty_generation: Cell::new(None),
        };
        let userdata = (&mut *terminal.callback_state as *mut TerminalCallbackState).cast();
        let glyph_protocol = false;
        unsafe {
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_USERDATA,
                userdata,
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_SIZE,
                (size_trampoline as *const ()).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_BELL,
                (bell_trampoline as *const ()).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_PWD_CHANGED,
                (pwd_changed_trampoline as *const ()).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE,
                (clipboard_write_trampoline as *const ()).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_SCHEME,
                (color_scheme_trampoline as *const ()).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                terminal.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL,
                (&glyph_protocol as *const bool).cast(),
            )
            .into_result()?;
        }
        Ok(terminal)
    }

    pub fn write(&mut self, bytes: &[u8]) {
        // SAFETY: self.raw is a live terminal handle for self's lifetime.
        unsafe {
            ffi::ghostty_terminal_vt_write(self.raw, bytes.as_ptr(), bytes.len());
        }
    }

    pub fn set_default_palette(&mut self, palette: &[RgbColor; 256]) -> Result<(), Error> {
        let palette = palette.map(|color| ffi::GhosttyColorRgb {
            r: color.r,
            g: color.g,
            b: color.b,
        });
        unsafe {
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_PALETTE,
                palette.as_ptr().cast(),
            )
            .into_result()
        }
    }

    pub fn default_palette(&self) -> Result<[RgbColor; 256], Error> {
        let mut out = [ffi::GhosttyColorRgb::default(); 256];
        // SAFETY: self.raw is a live terminal handle, and out is exactly the
        // 256-entry array this data kind writes.
        unsafe {
            ffi::ghostty_terminal_get(
                self.raw,
                ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT,
                out.as_mut_ptr().cast(),
            )
            .into_result()?;
        }
        Ok(out.map(Into::into))
    }

    pub fn resize(
        &mut self,
        cols: u16,
        rows: u16,
        cell_width_px: u32,
        cell_height_px: u32,
    ) -> Result<(), Error> {
        let size_report = ffi::GhosttySizeReportSize {
            rows,
            columns: cols,
            cell_width: cell_width_px,
            cell_height: cell_height_px,
        };
        // SAFETY: self.raw is valid and sizes are plain values.
        unsafe {
            ffi::ghostty_terminal_resize(
                self.raw,
                cols,
                rows,
                cell_width_px.max(1),
                cell_height_px.max(1),
            )
            .into_result()?;
        }
        self.callback_state.size_report = size_report;
        Ok(())
    }

    pub fn enable_kitty_graphics(&mut self) -> Result<(), Error> {
        install_png_decoder_once();
        let storage_limit = KITTY_IMAGE_STORAGE_LIMIT_BYTES;
        let enable_medium = true;
        unsafe {
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT,
                (&storage_limit as *const u64).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE,
                (&enable_medium as *const bool).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE,
                (&enable_medium as *const bool).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM,
                (&enable_medium as *const bool).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES,
                (&APC_MAX_BYTES as *const usize).cast(),
            )
            .into_result()?;
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY,
                (&APC_MAX_BYTES_KITTY as *const usize).cast(),
            )
            .into_result()?;
        }
        Ok(())
    }

    pub fn set_write_pty_callback<F>(&mut self, callback: F) -> Result<(), Error>
    where
        F: FnMut(&[u8]) + Send + 'static,
    {
        unsafe {
            ffi::ghostty_terminal_set(
                self.raw,
                ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_WRITE_PTY,
                (write_pty_trampoline as *const ()).cast(),
            )
            .into_result()?;
        }
        self.callback_state.write_pty = Some(Box::new(callback));
        Ok(())
    }

    pub fn set_color_scheme(&mut self, color_scheme: Option<ColorScheme>) -> Option<ColorScheme> {
        mem::replace(&mut self.callback_state.color_scheme, color_scheme)
    }

    pub fn take_bell_count(&mut self) -> u16 {
        mem::take(&mut self.callback_state.bell_count)
    }

    pub fn take_pwd_changes(&mut self) -> Vec<Vec<u8>> {
        mem::take(&mut self.callback_state.pwd_changes)
    }

    pub fn take_clipboard_writes(&mut self) -> Vec<Vec<u8>> {
        mem::take(&mut self.callback_state.clipboard_writes)
    }

    pub fn mode_get(&self, mode: u16) -> Result<bool, Error> {
        let mut out = false;
        unsafe { ffi::ghostty_terminal_mode_get(self.raw, mode, &mut out).into_result()? };
        Ok(out)
    }

    pub fn mode_set(&mut self, mode: u16, value: bool) -> Result<(), Error> {
        unsafe { ffi::ghostty_terminal_mode_set(self.raw, mode, value).into_result() }
    }

    pub fn kitty_keyboard_flags(&self) -> Result<u8, Error> {
        let mut out = 0u8;
        unsafe {
            ffi::ghostty_terminal_get(
                self.raw,
                ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS,
                (&mut out as *mut u8).cast(),
            )
            .into_result()?;
        }
        Ok(out)
    }

    pub fn mouse_tracking_enabled(&self) -> Result<bool, Error> {
        self.get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING)
    }

    pub fn modify_other_keys_enabled(&self) -> Result<bool, Error> {
        self.get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MODIFY_OTHER_KEYS)
    }

    pub fn active_screen(&self) -> Result<ActiveScreen, Error> {
        let mut out = ffi::GhosttyTerminalScreen_GHOSTTY_TERMINAL_SCREEN_PRIMARY;
        unsafe {
            ffi::ghostty_terminal_get(
                self.raw,
                ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN,
                (&mut out as *mut ffi::GhosttyTerminalScreen).cast(),
            )
            .into_result()?;
        }
        Ok(match out {
            ffi::GhosttyTerminalScreen_GHOSTTY_TERMINAL_SCREEN_PRIMARY => ActiveScreen::Primary,
            ffi::GhosttyTerminalScreen_GHOSTTY_TERMINAL_SCREEN_ALTERNATE => ActiveScreen::Alternate,
            _ => ActiveScreen::Primary,
        })
    }

    pub fn total_rows(&self) -> Result<usize, Error> {
        self.get_usize(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_TOTAL_ROWS)
    }

    pub fn scrollback_rows(&self) -> Result<usize, Error> {
        self.get_usize(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS)
    }

    pub fn max_scrollback(&self) -> usize {
        self.max_scrollback
    }

    pub fn scrollbar(&self) -> Result<TerminalScrollbar, Error> {
        let mut out = ffi::GhosttyTerminalScrollbar::default();
        unsafe {
            ffi::ghostty_terminal_get(
                self.raw,
                ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_SCROLLBAR,
                (&mut out as *mut ffi::GhosttyTerminalScrollbar).cast(),
            )
            .into_result()?;
        }
        Ok(TerminalScrollbar {
            total: out.total as usize,
            offset: out.offset as usize,
            len: out.len as usize,
        })
    }

    #[cfg(windows)]
    pub(crate) fn track_row(&mut self, y: u32) -> Option<usize> {
        let mut point = ffi::GhosttyPointCoordinate::default();
        let tag = ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN;
        let result =
            unsafe { ffi::ghostty_tracked_grid_ref_point(self.tracked_row, tag, &mut point) };
        let terminal = self.raw;
        let target = ghostty_viewport_point(0, y);
        unsafe {
            ffi::ghostty_tracked_grid_ref_free(self.tracked_row);
            self.tracked_row = ptr::null_mut();
            let _ = ffi::ghostty_terminal_grid_ref_track(terminal, target, &mut self.tracked_row);
        }
        (result == ffi::GhosttyResult_GHOSTTY_SUCCESS).then_some(point.y as usize)
    }

    pub fn screen_cell(&self, x: u16, y: u32) -> Result<(CellWide, Vec<u32>), Error> {
        let grid_ref = self.grid_ref(ghostty_screen_point(x, y))?;
        let wide = grid_ref_wide(&grid_ref)?;
        let graphemes = grid_ref_graphemes(&grid_ref)?;
        Ok((wide, graphemes))
    }

    pub(crate) fn screen_text_rows(&self) -> Result<Vec<ScreenTextRow>, Error> {
        self.screen_text_rows_range(0, usize::MAX)
    }

    pub(crate) fn screen_text_rows_range(
        &self,
        start_row: usize,
        end_row_exclusive: usize,
    ) -> Result<Vec<ScreenTextRow>, Error> {
        let total_rows = self.total_rows()?;
        let start_row = start_row.min(total_rows);
        let end_row_exclusive = end_row_exclusive.min(total_rows).max(start_row);
        let cols = self.cols()?;
        let mut rows = Vec::with_capacity(end_row_exclusive.saturating_sub(start_row));
        for y in start_row..end_row_exclusive {
            let Some(y) = u32::try_from(y).ok() else {
                break;
            };
            let mut grid_ref = self.grid_ref(ghostty_screen_point(0, y))?;
            let (soft_wrapped, wrap_continuation) = grid_ref_wrap_state(&grid_ref)?;
            let mut cells = Vec::with_capacity(usize::from(cols));
            for x in 0..cols {
                grid_ref.x = x;
                cells.push(ScreenTextCell {
                    wide: grid_ref_wide(&grid_ref)?,
                    graphemes: grid_ref_graphemes(&grid_ref)?,
                });
            }
            rows.push(ScreenTextRow {
                cells,
                soft_wrapped,
                wrap_continuation,
            });
        }
        Ok(rows)
    }

    fn viewport_graphemes_and_style(&self, x: u16, y: u32) -> Result<(Vec<u32>, CellStyle), Error> {
        let grid_ref = self.grid_ref(ghostty_viewport_point(x, y))?;
        let graphemes = grid_ref_graphemes(&grid_ref)?;
        let mut style = ffi::GhosttyStyle {
            size: mem::size_of::<ffi::GhosttyStyle>(),
            ..Default::default()
        };
        unsafe {
            ffi::ghostty_grid_ref_style(&grid_ref, &mut style).into_result()?;
        }
        Ok((graphemes, style.into()))
    }

    pub fn viewport_hyperlink_uri(&self, x: u16, y: u32) -> Result<Option<String>, Error> {
        let grid_ref = self.grid_ref(ghostty_viewport_point(x, y))?;
        grid_ref_hyperlink_uri(&grid_ref)
    }

    fn grid_ref(&self, point: ffi::GhosttyPoint) -> Result<ffi::GhosttyGridRef, Error> {
        let mut grid_ref = ffi::GhosttyGridRef {
            size: mem::size_of::<ffi::GhosttyGridRef>(),
            ..Default::default()
        };
        unsafe {
            ffi::ghostty_terminal_grid_ref(self.raw, point, &mut grid_ref).into_result()?;
        }
        Ok(grid_ref)
    }

    pub fn read_text_viewport(
        &self,
        start: (u16, u32),
        end: (u16, u32),
        rectangle: bool,
    ) -> Result<String, Error> {
        self.read_formatted_selection(
            ghostty_viewport_point(start.0, start.1),
            ghostty_viewport_point(end.0, end.1),
            rectangle,
            FormatterFormat::Plain,
            true,
            true,
        )
    }

    pub fn read_ansi_viewport(
        &self,
        start: (u16, u32),
        end: (u16, u32),
        rectangle: bool,
    ) -> Result<String, Error> {
        self.read_formatted_selection(
            ghostty_viewport_point(start.0, start.1),
            ghostty_viewport_point(end.0, end.1),
            rectangle,
            FormatterFormat::Vt,
            false,
            true,
        )
    }

    pub fn read_text_screen(
        &self,
        start: (u16, u32),
        end: (u16, u32),
        rectangle: bool,
    ) -> Result<String, Error> {
        self.read_formatted_selection(
            ghostty_screen_point(start.0, start.1),
            ghostty_screen_point(end.0, end.1),
            rectangle,
            FormatterFormat::Plain,
            true,
            true,
        )
    }

    pub fn read_ansi_screen(
        &self,
        start: (u16, u32),
        end: (u16, u32),
        rectangle: bool,
        unwrap: bool,
    ) -> Result<String, Error> {
        self.read_formatted_selection(
            ghostty_screen_point(start.0, start.1),
            ghostty_screen_point(end.0, end.1),
            rectangle,
            FormatterFormat::Vt,
            unwrap,
            true,
        )
    }

    pub fn keyboard_state_ansi(&self) -> Result<String, Error> {
        self.format_keyboard_state_ansi(false)
    }

    pub fn kitty_keyboard_state_ansi(&self) -> Result<String, Error> {
        self.format_keyboard_state_ansi(true)
    }

    fn format_keyboard_state_ansi(&self, kitty_keyboard: bool) -> Result<String, Error> {
        let mut formatter: ffi::GhosttyFormatter = ptr::null_mut();
        let options = ffi::GhosttyFormatterTerminalOptions {
            size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(),
            emit: FormatterFormat::Vt.as_raw(),
            unwrap: false,
            trim: false,
            extra: ffi::GhosttyFormatterTerminalExtra {
                size: mem::size_of::<ffi::GhosttyFormatterTerminalExtra>(),
                keyboard: true,
                screen: ffi::GhosttyFormatterScreenExtra {
                    size: mem::size_of::<ffi::GhosttyFormatterScreenExtra>(),
                    kitty_keyboard,
                    ..Default::default()
                },
                ..Default::default()
            },
            selection: ptr::null(),
        };
        unsafe {
            ffi::ghostty_formatter_terminal_new(ptr::null(), &mut formatter, self.raw, options)
                .into_result()?;
        }

        let mut out_ptr = ptr::null_mut();
        let mut out_len = 0usize;
        let result = unsafe {
            ffi::ghostty_formatter_format_alloc(formatter, ptr::null(), &mut out_ptr, &mut out_len)
        };
        unsafe {
            ffi::ghostty_formatter_free(formatter);
        }
        result.into_result()?;

        let text = if out_len == 0 {
            String::new()
        } else {
            let bytes = unsafe { slice::from_raw_parts(out_ptr.cast_const(), out_len) };
            String::from_utf8_lossy(bytes).into_owned()
        };

        if !out_ptr.is_null() {
            unsafe {
                ffi::ghostty_free(ptr::null(), out_ptr, out_len);
            }
        }

        Ok(text)
    }

    fn read_formatted_selection(
        &self,
        start: ffi::GhosttyPoint,
        end: ffi::GhosttyPoint,
        rectangle: bool,
        format: FormatterFormat,
        unwrap: bool,
        trim: bool,
    ) -> Result<String, Error> {
        let mut start_ref = ffi::GhosttyGridRef {
            size: mem::size_of::<ffi::GhosttyGridRef>(),
            ..Default::default()
        };
        let mut end_ref = ffi::GhosttyGridRef {
            size: mem::size_of::<ffi::GhosttyGridRef>(),
            ..Default::default()
        };
        unsafe {
            ffi::ghostty_terminal_grid_ref(self.raw, start, &mut start_ref).into_result()?;
            ffi::ghostty_terminal_grid_ref(self.raw, end, &mut end_ref).into_result()?;
        }

        let selection = ffi::GhosttySelection {
            size: mem::size_of::<ffi::GhosttySelection>(),
            start: start_ref,
            end: end_ref,
            rectangle,
        };
        let mut formatter: ffi::GhosttyFormatter = ptr::null_mut();
        let options = ffi::GhosttyFormatterTerminalOptions {
            size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(),
            emit: format.as_raw(),
            unwrap,
            trim,
            extra: ffi::GhosttyFormatterTerminalExtra {
                size: mem::size_of::<ffi::GhosttyFormatterTerminalExtra>(),
                screen: ffi::GhosttyFormatterScreenExtra {
                    size: mem::size_of::<ffi::GhosttyFormatterScreenExtra>(),
                    ..Default::default()
                },
                ..Default::default()
            },
            selection: &selection,
        };
        unsafe {
            ffi::ghostty_formatter_terminal_new(ptr::null(), &mut formatter, self.raw, options)
                .into_result()?;
        }

        let mut out_ptr = ptr::null_mut();
        let mut out_len = 0usize;
        let result = unsafe {
            ffi::ghostty_formatter_format_alloc(formatter, ptr::null(), &mut out_ptr, &mut out_len)
        };
        unsafe {
            ffi::ghostty_formatter_free(formatter);
        }
        result.into_result()?;

        let text = if out_len == 0 {
            String::new()
        } else {
            let bytes = unsafe { slice::from_raw_parts(out_ptr.cast_const(), out_len) };
            String::from_utf8_lossy(bytes).into_owned()
        };

        if !out_ptr.is_null() {
            unsafe {
                ffi::ghostty_free(ptr::null(), out_ptr, out_len);
            }
        }

        Ok(text)
    }

    pub fn scroll_viewport_bottom(&mut self) {
        let viewport = ffi::GhosttyTerminalScrollViewport {
            tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_BOTTOM,
            value: ffi::GhosttyTerminalScrollViewportValue::default(),
        };
        // SAFETY: self.raw is valid and viewport value matches the tag.
        unsafe {
            ffi::ghostty_terminal_scroll_viewport(self.raw, viewport);
        }
    }

    pub fn scroll_viewport_delta(&mut self, delta: isize) {
        let viewport = ffi::GhosttyTerminalScrollViewport {
            tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_DELTA,
            value: ffi::GhosttyTerminalScrollViewportValue { delta },
        };
        // SAFETY: self.raw is valid and viewport value matches the tag.
        unsafe {
            ffi::ghostty_terminal_scroll_viewport(self.raw, viewport);
        }
    }

    pub fn scroll_viewport_row(&mut self, row: usize) {
        let viewport = ffi::GhosttyTerminalScrollViewport {
            tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_ROW,
            value: ffi::GhosttyTerminalScrollViewportValue { row },
        };
        // SAFETY: self.raw is valid and viewport value matches the tag.
        unsafe {
            ffi::ghostty_terminal_scroll_viewport(self.raw, viewport);
        }
    }

    pub fn cols(&self) -> Result<u16, Error> {
        self.get_u16(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_COLS)
    }

    pub fn rows(&self) -> Result<u16, Error> {
        self.get_u16(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ROWS)
    }

    pub fn effective_foreground_color(&self) -> Result<Option<RgbColor>, Error> {
        self.get_optional_rgb_color(TERMINAL_DATA_COLOR_FOREGROUND)
    }

    pub fn effective_cursor_color(&self) -> Result<Option<RgbColor>, Error> {
        self.get_optional_rgb_color(TERMINAL_DATA_COLOR_CURSOR)
    }

    pub(crate) fn width_px(&self) -> Result<u32, Error> {
        self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_WIDTH_PX)
    }

    pub(crate) fn height_px(&self) -> Result<u32, Error> {
        self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_HEIGHT_PX)
    }

    fn get_u16(&self, data: ffi::GhosttyTerminalData) -> Result<u16, Error> {
        let mut out = 0u16;
        // SAFETY: out points to a u16 matching the requested terminal data type.
        unsafe {
            ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut u16).cast())
                .into_result()?;
        }
        Ok(out)
    }

    fn get_u32(&self, data: ffi::GhosttyTerminalData) -> Result<u32, Error> {
        let mut out = 0u32;
        // SAFETY: out points to a u32 matching the requested terminal data type.
        unsafe {
            ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut u32).cast())
                .into_result()?;
        }
        Ok(out)
    }

    fn get_usize(&self, data: ffi::GhosttyTerminalData) -> Result<usize, Error> {
        let mut out = 0usize;
        unsafe {
            ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut usize).cast())
                .into_result()?;
        }
        Ok(out)
    }

    fn get_bool(&self, data: ffi::GhosttyTerminalData) -> Result<bool, Error> {
        let mut out = false;
        unsafe {
            ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut bool).cast())
                .into_result()?;
        }
        Ok(out)
    }

    fn get_optional_rgb_color(
        &self,
        data: ffi::GhosttyTerminalData,
    ) -> Result<Option<RgbColor>, Error> {
        let mut out = ffi::GhosttyColorRgb::default();
        let result = unsafe {
            ffi::ghostty_terminal_get(
                self.raw,
                data,
                (&mut out as *mut ffi::GhosttyColorRgb).cast(),
            )
        };
        match result {
            ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(out.into())),
            ffi::GhosttyResult_GHOSTTY_NO_VALUE => Ok(None),
            other => Err(Error(other)),
        }
    }

    fn kitty_graphics(&self) -> Result<ffi::GhosttyKittyGraphics, Error> {
        let mut graphics: ffi::GhosttyKittyGraphics = ptr::null_mut();
        unsafe {
            ffi::ghostty_terminal_get(
                self.raw,
                ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS,
                (&mut graphics as *mut ffi::GhosttyKittyGraphics).cast(),
            )
            .into_result()?;
        }
        Ok(graphics)
    }

    pub fn kitty_graphics_generation(&self) -> Result<u64, Error> {
        let graphics = self.kitty_graphics()?;
        if graphics.is_null() {
            return Ok(0);
        }
        kitty_graphics_u64(
            graphics,
            ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION,
        )
    }

    pub fn kitty_image_placements(&self) -> Result<Vec<KittyImagePlacement>, Error> {
        self.kitty_image_placements_with_data_filter(|_| true)
    }

    pub fn kitty_image_placements_with_data_filter<F>(
        &self,
        mut needs_data: F,
    ) -> Result<Vec<KittyImagePlacement>, Error>
    where
        F: FnMut(KittyImageDescriptor) -> bool,
    {
        let graphics = self.kitty_graphics()?;
        if graphics.is_null() {
            return Ok(Vec::new());
        }
        let generation = kitty_graphics_u64(
            graphics,
            ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION,
        )?;
        if generation == 0 || self.kitty_empty_generation.get() == Some(generation) {
            return Ok(Vec::new());
        }

        let mut iterator: ffi::GhosttyKittyGraphicsPlacementIterator = ptr::null_mut();
        unsafe {
            ffi::ghostty_kitty_graphics_placement_iterator_new(ptr::null(), &mut iterator)
                .into_result()?;
            ffi::ghostty_kitty_graphics_get(
                graphics,
                ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR,
                (&mut iterator as *mut ffi::GhosttyKittyGraphicsPlacementIterator).cast(),
            )
            .into_result()?;
        }
        let _guard = KittyPlacementIteratorGuard { raw: iterator };

        let mut placements = Vec::new();
        let mut storage_has_placements = false;
        while unsafe { ffi::ghostty_kitty_graphics_placement_next(iterator) } {
            storage_has_placements = true;
            if let Some(placement) =
                self.kitty_image_placement(graphics, iterator, &mut needs_data)?
            {
                placements.push(placement);
            }
        }
        if !storage_has_placements {
            self.kitty_empty_generation.set(Some(generation));
            self.prune_kitty_fingerprints(&[]);
            return Ok(Vec::new());
        }

        placements.extend(self.kitty_virtual_image_placements(graphics, &mut needs_data)?);
        placements.sort_by_key(|placement| placement.z);
        self.prune_kitty_fingerprints(&placements);
        Ok(placements)
    }

    /// Fingerprint for `image`, cached per image id and recomputed only when
    /// the image's generation changes.
    fn kitty_image_fingerprint_cached(
        &self,
        image: ffi::GhosttyKittyGraphicsImage,
        image_id: u32,
        data: (*const u8, usize),
        image_width: u32,
        image_height: u32,
        format: KittyImageFormat,
    ) -> u64 {
        let (data_ptr, data_len) = data;
        let Ok(generation) = kitty_image_u64(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_GENERATION,
        ) else {
            return kitty_image_fingerprint(data_ptr, data_len, image_width, image_height, format);
        };

        if let Ok(cache) = self.kitty_fingerprints.lock() {
            if let Some(entry) = cache.get(&image_id) {
                if entry.generation == generation {
                    return entry.fingerprint;
                }
            }
        }

        let fingerprint =
            kitty_image_fingerprint(data_ptr, data_len, image_width, image_height, format);
        if let Ok(mut cache) = self.kitty_fingerprints.lock() {
            cache.insert(
                image_id,
                KittyImageFingerprintEntry {
                    generation,
                    fingerprint,
                },
            );
        }
        fingerprint
    }

    fn prune_kitty_fingerprints(&self, placements: &[KittyImagePlacement]) {
        if let Ok(mut cache) = self.kitty_fingerprints.lock() {
            if cache.is_empty() {
                return;
            }
            let live: HashSet<u32> = placements
                .iter()
                .map(|placement| placement.image_id)
                .collect();
            cache.retain(|image_id, _| live.contains(image_id));
        }
    }

    fn kitty_image_placement<F>(
        &self,
        graphics: ffi::GhosttyKittyGraphics,
        iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
        needs_data: &mut F,
    ) -> Result<Option<KittyImagePlacement>, Error>
    where
        F: FnMut(KittyImageDescriptor) -> bool,
    {
        let image_id = kitty_placement_u32(
            iterator,
            ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID,
        )?;
        if kitty_placement_bool(iterator, KITTY_PLACEMENT_DATA_IS_VIRTUAL)? {
            return Ok(None);
        }
        let image = unsafe { ffi::ghostty_kitty_graphics_image(graphics, image_id) };
        if image.is_null() {
            return Ok(None);
        }

        let mut raw_info = ffi::GhosttyKittyGraphicsPlacementRenderInfo {
            size: mem::size_of::<ffi::GhosttyKittyGraphicsPlacementRenderInfo>(),
            ..Default::default()
        };
        unsafe {
            ffi::ghostty_kitty_graphics_placement_render_info(
                iterator,
                image,
                self.raw,
                &mut raw_info,
            )
            .into_result()?;
        }
        if !raw_info.viewport_visible {
            return Ok(None);
        }

        let image_width = kitty_image_u32(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_WIDTH,
        )?;
        let image_height = kitty_image_u32(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_HEIGHT,
        )?;
        let format = kitty_image_format(image)?;
        let compression = kitty_image_compression(image)?;
        if compression != ffi::GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE {
            return Ok(None);
        }
        let placement_id = kitty_placement_u32(
            iterator,
            ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID,
        )?;
        let (data_ptr, data_len) = kitty_image_data_ptr_len(image)?;
        let data_fingerprint = self.kitty_image_fingerprint_cached(
            image,
            image_id,
            (data_ptr, data_len),
            image_width,
            image_height,
            format,
        );
        let descriptor = KittyImageDescriptor {
            image_id,
            placement_id,
            image_width,
            image_height,
            format,
            data_len,
            data_fingerprint,
        };
        let data = if needs_data(descriptor) {
            kitty_image_data_from_ptr(data_ptr, data_len)
        } else {
            Vec::new()
        };
        let x_offset = kitty_placement_u32(
            iterator,
            ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET,
        )?;
        let y_offset = kitty_placement_u32(
            iterator,
            ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET,
        )?;
        let z = kitty_placement_i32(
            iterator,
            ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z,
        )?;

        Ok(Some(KittyImagePlacement {
            image_id,
            placement_id,
            z,
            x_offset,
            y_offset,
            image_width,
            image_height,
            format,
            data_len,
            data_fingerprint,
            data,
            render: KittyPlacementRenderInfo {
                pixel_width: raw_info.pixel_width,
                pixel_height: raw_info.pixel_height,
                grid_cols: raw_info.grid_cols,
                grid_rows: raw_info.grid_rows,
                viewport_col: raw_info.viewport_col,
                viewport_row: raw_info.viewport_row,
                source_x: raw_info.source_x,
                source_y: raw_info.source_y,
                source_width: raw_info.source_width,
                source_height: raw_info.source_height,
            },
        }))
    }

    fn kitty_virtual_image_placements<F>(
        &self,
        graphics: ffi::GhosttyKittyGraphics,
        needs_data: &mut F,
    ) -> Result<Vec<KittyImagePlacement>, Error>
    where
        F: FnMut(KittyImageDescriptor) -> bool,
    {
        let specs = kitty_virtual_placement_specs(graphics)?;
        if specs.is_empty() {
            return Ok(Vec::new());
        }

        let viewport_cols = self.cols()?.max(1);
        let viewport_rows = self.rows()?.max(1);
        let cell_width = (self.width_px()? / u32::from(viewport_cols)).max(1);
        let cell_height = (self.height_px()? / u32::from(viewport_rows)).max(1);
        let mut runs = Vec::new();
        for y in 0..viewport_rows {
            let mut current: Option<KittyVirtualRun> = None;
            for x in 0..viewport_cols {
                let (graphemes, style) = self.viewport_graphemes_and_style(x, u32::from(y))?;
                let cell = kitty_virtual_cell(x, y, &graphemes, style);
                match cell {
                    Some(cell) => {
                        if let Some(run) = current.as_mut() {
                            if run.append(cell) {
                                continue;
                            }
                            runs.push(*run);
                        }
                        current = Some(KittyVirtualRun::from_cell(cell));
                    }
                    None => {
                        if let Some(run) = current.take() {
                            runs.push(run);
                        }
                    }
                }
            }
            if let Some(run) = current {
                runs.push(run);
            }
        }

        let mut placements = Vec::new();
        for run in runs {
            let image_id = run.image_id();
            let Some(spec) = find_virtual_placement_spec(&specs, image_id, run.placement_id())
            else {
                continue;
            };
            let image = unsafe { ffi::ghostty_kitty_graphics_image(graphics, image_id) };
            if image.is_null() {
                continue;
            }
            let image_width = kitty_image_u32(
                image,
                ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_WIDTH,
            )?;
            let image_height = kitty_image_u32(
                image,
                ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_HEIGHT,
            )?;
            let format = kitty_image_format(image)?;
            let compression = kitty_image_compression(image)?;
            if compression != ffi::GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE
            {
                continue;
            }
            let Some(geometry) = kitty_virtual_placement_geometry(
                run,
                *spec,
                image_width,
                image_height,
                cell_width,
                cell_height,
            ) else {
                continue;
            };
            let placement_id = run.synthetic_placement_id();
            let (data_ptr, data_len) = kitty_image_data_ptr_len(image)?;
            let data_fingerprint = self.kitty_image_fingerprint_cached(
                image,
                image_id,
                (data_ptr, data_len),
                image_width,
                image_height,
                format,
            );
            let descriptor = KittyImageDescriptor {
                image_id,
                placement_id,
                image_width,
                image_height,
                format,
                data_len,
                data_fingerprint,
            };
            let data = if needs_data(descriptor) {
                kitty_image_data_from_ptr(data_ptr, data_len)
            } else {
                Vec::new()
            };
            placements.push(KittyImagePlacement {
                image_id,
                placement_id,
                z: spec.z,
                x_offset: geometry.x_offset,
                y_offset: geometry.y_offset,
                image_width,
                image_height,
                format,
                data_len,
                data_fingerprint,
                data,
                render: geometry.render,
            });
        }

        Ok(placements)
    }

    fn raw(&self) -> ffi::GhosttyTerminal {
        self.raw
    }
}

struct KittyPlacementIteratorGuard {
    raw: ffi::GhosttyKittyGraphicsPlacementIterator,
}

impl Drop for KittyPlacementIteratorGuard {
    fn drop(&mut self) {
        unsafe { ffi::ghostty_kitty_graphics_placement_iterator_free(self.raw) }
    }
}

// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
unsafe impl Send for Terminal {}

impl Drop for Terminal {
    fn drop(&mut self) {
        // SAFETY: freeing a null or live handle is allowed by the C API.
        unsafe {
            #[cfg(windows)]
            ffi::ghostty_tracked_grid_ref_free(self.tracked_row);
            ffi::ghostty_terminal_free(self.raw);
        }
    }
}

fn ghostty_viewport_point(x: u16, y: u32) -> ffi::GhosttyPoint {
    ffi::GhosttyPoint {
        tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_VIEWPORT,
        value: ffi::GhosttyPointValue {
            coordinate: ffi::GhosttyPointCoordinate { x, y },
        },
    }
}

fn ghostty_screen_point(x: u16, y: u32) -> ffi::GhosttyPoint {
    ffi::GhosttyPoint {
        tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN,
        value: ffi::GhosttyPointValue {
            coordinate: ffi::GhosttyPointCoordinate { x, y },
        },
    }
}

fn grid_ref_graphemes(grid_ref: &ffi::GhosttyGridRef) -> Result<Vec<u32>, Error> {
    let mut required = 0usize;
    let result =
        unsafe { ffi::ghostty_grid_ref_graphemes(grid_ref, ptr::null_mut(), 0, &mut required) };
    if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
        result.into_result()?;
    }
    let mut buffer = vec![0u32; required];
    if required == 0 {
        return Ok(buffer);
    }
    unsafe {
        ffi::ghostty_grid_ref_graphemes(grid_ref, buffer.as_mut_ptr(), buffer.len(), &mut required)
            .into_result()?;
    }
    buffer.truncate(required);
    Ok(buffer)
}

fn grid_ref_wide(grid_ref: &ffi::GhosttyGridRef) -> Result<CellWide, Error> {
    let mut raw = ffi::GhosttyCell::default();
    unsafe {
        ffi::ghostty_grid_ref_cell(grid_ref, &mut raw).into_result()?;
    }

    let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW;
    unsafe {
        ffi::ghostty_cell_get(
            raw,
            ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE,
            (&mut wide as *mut ffi::GhosttyCellWide).cast(),
        )
        .into_result()?;
    }
    Ok(CellWide::from_raw(wide))
}

fn grid_ref_wrap_state(grid_ref: &ffi::GhosttyGridRef) -> Result<(bool, bool), Error> {
    let mut row = 0;
    unsafe {
        ffi::ghostty_grid_ref_row(grid_ref, &mut row).into_result()?;
    }
    let mut soft_wrapped = false;
    let mut wrap_continuation = false;
    unsafe {
        ffi::ghostty_row_get(
            row,
            ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP,
            (&mut soft_wrapped as *mut bool).cast(),
        )
        .into_result()?;
        ffi::ghostty_row_get(
            row,
            ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP_CONTINUATION,
            (&mut wrap_continuation as *mut bool).cast(),
        )
        .into_result()?;
    }
    Ok((soft_wrapped, wrap_continuation))
}

fn kitty_placement_u32(
    iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
    data: ffi::GhosttyKittyGraphicsPlacementData,
) -> Result<u32, Error> {
    let mut out = 0u32;
    unsafe {
        ffi::ghostty_kitty_graphics_placement_get(iterator, data, (&mut out as *mut u32).cast())
            .into_result()?;
    }
    Ok(out)
}

fn kitty_placement_i32(
    iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
    data: ffi::GhosttyKittyGraphicsPlacementData,
) -> Result<i32, Error> {
    let mut out = 0i32;
    unsafe {
        ffi::ghostty_kitty_graphics_placement_get(iterator, data, (&mut out as *mut i32).cast())
            .into_result()?;
    }
    Ok(out)
}

fn kitty_placement_bool(
    iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
    data: ffi::GhosttyKittyGraphicsPlacementData,
) -> Result<bool, Error> {
    let mut out = false;
    unsafe {
        ffi::ghostty_kitty_graphics_placement_get(iterator, data, (&mut out as *mut bool).cast())
            .into_result()?;
    }
    Ok(out)
}

fn kitty_virtual_placement_specs(
    graphics: ffi::GhosttyKittyGraphics,
) -> Result<Vec<KittyVirtualPlacementSpec>, Error> {
    let mut iterator: ffi::GhosttyKittyGraphicsPlacementIterator = ptr::null_mut();
    unsafe {
        ffi::ghostty_kitty_graphics_placement_iterator_new(ptr::null(), &mut iterator)
            .into_result()?;
        ffi::ghostty_kitty_graphics_get(
            graphics,
            ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR,
            (&mut iterator as *mut ffi::GhosttyKittyGraphicsPlacementIterator).cast(),
        )
        .into_result()?;
    }
    let _guard = KittyPlacementIteratorGuard { raw: iterator };

    let mut specs = Vec::new();
    while unsafe { ffi::ghostty_kitty_graphics_placement_next(iterator) } {
        if !kitty_placement_bool(iterator, KITTY_PLACEMENT_DATA_IS_VIRTUAL)? {
            continue;
        }
        specs.push(KittyVirtualPlacementSpec {
            image_id: kitty_placement_u32(
                iterator,
                ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID,
            )?,
            placement_id: kitty_placement_u32(
                iterator,
                ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID,
            )?,
            columns: kitty_placement_u32(iterator, KITTY_PLACEMENT_DATA_COLUMNS)?,
            rows: kitty_placement_u32(iterator, KITTY_PLACEMENT_DATA_ROWS)?,
            z: kitty_placement_i32(
                iterator,
                ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z,
            )?,
        });
    }
    Ok(specs)
}

fn find_virtual_placement_spec(
    specs: &[KittyVirtualPlacementSpec],
    image_id: u32,
    placement_id: u32,
) -> Option<&KittyVirtualPlacementSpec> {
    if placement_id > 0 {
        specs
            .iter()
            .find(|spec| spec.image_id == image_id && spec.placement_id == placement_id)
    } else {
        specs.iter().find(|spec| spec.image_id == image_id)
    }
}

fn kitty_virtual_cell(
    x: u16,
    y: u16,
    graphemes: &[u32],
    style: CellStyle,
) -> Option<KittyVirtualCell> {
    if graphemes.first().copied() != Some(KITTY_UNICODE_PLACEHOLDER) {
        return None;
    }
    let image_id_low = style
        .fg_color
        .map(kitty_placeholder_color_to_id)
        .unwrap_or(0);
    let placement_id = style
        .underline_color
        .map(kitty_placeholder_color_to_id)
        .filter(|id| *id != 0);
    let row = graphemes
        .get(1)
        .and_then(|codepoint| kitty_placeholder_diacritic_index(*codepoint));
    let col = graphemes
        .get(2)
        .and_then(|codepoint| kitty_placeholder_diacritic_index(*codepoint));
    let image_id_high = graphemes
        .get(3)
        .and_then(|codepoint| kitty_placeholder_diacritic_index(*codepoint))
        .filter(|high| *high <= u32::from(u8::MAX));

    Some(KittyVirtualCell {
        x,
        y,
        image_id_low,
        image_id_high,
        placement_id,
        row,
        col,
    })
}

fn kitty_placeholder_color_to_id(color: CellColor) -> u32 {
    match color {
        CellColor::Palette(value) => value.into(),
        CellColor::Rgb(color) => {
            (u32::from(color.r) << 16) | (u32::from(color.g) << 8) | u32::from(color.b)
        }
    }
}

fn kitty_placeholder_diacritic_index(codepoint: u32) -> Option<u32> {
    let map = KITTY_PLACEHOLDER_DIACRITICS.get_or_init(|| {
        // Reuse Ghostty's vendored table so Herdr decodes the same placeholder
        // row/column diacritics that libghostty accepts.
        let source =
            include_str!("../../vendor/libghostty-vt/src/terminal/kitty/graphics_unicode.zig");
        let mut map = HashMap::new();
        let mut in_table = false;
        for line in source.lines() {
            let line = line.trim();
            if line.starts_with("const diacritics:") {
                in_table = true;
                continue;
            }
            if !in_table {
                continue;
            }
            if line == "};" {
                break;
            }
            let Some(hex) = line
                .strip_prefix("0x")
                .and_then(|value| value.strip_suffix(','))
            else {
                continue;
            };
            if let Ok(value) = u32::from_str_radix(hex, 16) {
                map.insert(value, map.len() as u32);
            }
        }
        map
    });
    map.get(&codepoint).copied()
}

fn kitty_virtual_placement_geometry(
    run: KittyVirtualRun,
    spec: KittyVirtualPlacementSpec,
    image_width: u32,
    image_height: u32,
    cell_width: u32,
    cell_height: u32,
) -> Option<KittyVirtualPlacementGeometry> {
    let grid_cols = if spec.columns == 0 {
        image_width.saturating_add(cell_width - 1) / cell_width
    } else {
        spec.columns
    }
    .max(1);
    let grid_rows = if spec.rows == 0 {
        image_height.saturating_add(cell_height - 1) / cell_height
    } else {
        spec.rows
    }
    .max(1);

    if run.col >= grid_cols || run.row >= grid_rows {
        return None;
    }
    let visible_cols = run.width.min(grid_cols.saturating_sub(run.col)).max(1);
    let visible_rows = 1;
    let source_x = scale_u32(run.col, image_width, grid_cols);
    let source_y = scale_u32(run.row, image_height, grid_rows);
    let source_width = scale_u32(visible_cols, image_width, grid_cols)
        .max(1)
        .min(image_width.saturating_sub(source_x));
    let source_height = scale_u32(visible_rows, image_height, grid_rows)
        .max(1)
        .min(image_height.saturating_sub(source_y));
    if source_width == 0 || source_height == 0 {
        return None;
    }

    Some(KittyVirtualPlacementGeometry {
        x_offset: 0,
        y_offset: 0,
        render: KittyPlacementRenderInfo {
            pixel_width: visible_cols.saturating_mul(cell_width).max(1),
            pixel_height: visible_rows.saturating_mul(cell_height).max(1),
            grid_cols: visible_cols,
            grid_rows: visible_rows,
            viewport_col: i32::from(run.x),
            viewport_row: i32::from(run.y),
            source_x,
            source_y,
            source_width,
            source_height,
        },
    })
}

fn scale_u32(value: u32, source: u32, dest: u32) -> u32 {
    ((u64::from(value)).saturating_mul(u64::from(source)) / u64::from(dest.max(1)))
        .min(u64::from(u32::MAX)) as u32
}

impl KittyVirtualRun {
    fn from_cell(cell: KittyVirtualCell) -> Self {
        Self {
            x: cell.x,
            y: cell.y,
            image_id_low: cell.image_id_low,
            image_id_high: cell.image_id_high,
            placement_id: cell.placement_id,
            row: cell.row.unwrap_or(0),
            col: cell.col.unwrap_or(0),
            width: 1,
        }
    }

    fn append(&mut self, cell: KittyVirtualCell) -> bool {
        if self.image_id_low != cell.image_id_low
            || self.placement_id != cell.placement_id
            || cell.row.is_some_and(|row| row != self.row)
            || cell.col.is_some_and(|col| col != self.col + self.width)
            || cell
                .image_id_high
                .is_some_and(|high| Some(high) != self.image_id_high)
        {
            return false;
        }
        self.width += 1;
        true
    }

    fn image_id(self) -> u32 {
        self.image_id_low | (self.image_id_high.unwrap_or(0) << 24)
    }

    fn placement_id(self) -> u32 {
        self.placement_id.unwrap_or(0)
    }

    fn synthetic_placement_id(self) -> u32 {
        let mut hasher = DefaultHasher::new();
        self.image_id().hash(&mut hasher);
        self.placement_id().hash(&mut hasher);
        self.row.hash(&mut hasher);
        self.col.hash(&mut hasher);
        self.width.hash(&mut hasher);
        self.x.hash(&mut hasher);
        self.y.hash(&mut hasher);
        1 + ((hasher.finish() as u32) % 900_000)
    }
}

fn kitty_graphics_u64(
    graphics: ffi::GhosttyKittyGraphics,
    data: ffi::GhosttyKittyGraphicsData,
) -> Result<u64, Error> {
    let mut out = 0u64;
    unsafe {
        ffi::ghostty_kitty_graphics_get(graphics, data, (&mut out as *mut u64).cast())
            .into_result()?;
    }
    Ok(out)
}

fn kitty_image_u32(
    image: ffi::GhosttyKittyGraphicsImage,
    data: ffi::GhosttyKittyGraphicsImageData,
) -> Result<u32, Error> {
    let mut out = 0u32;
    unsafe {
        ffi::ghostty_kitty_graphics_image_get(image, data, (&mut out as *mut u32).cast())
            .into_result()?;
    }
    Ok(out)
}

fn kitty_image_u64(
    image: ffi::GhosttyKittyGraphicsImage,
    data: ffi::GhosttyKittyGraphicsImageData,
) -> Result<u64, Error> {
    let mut out = 0u64;
    unsafe {
        ffi::ghostty_kitty_graphics_image_get(image, data, (&mut out as *mut u64).cast())
            .into_result()?;
    }
    Ok(out)
}

fn kitty_image_format(image: ffi::GhosttyKittyGraphicsImage) -> Result<KittyImageFormat, Error> {
    let mut out = ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGBA;
    unsafe {
        ffi::ghostty_kitty_graphics_image_get(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_FORMAT,
            (&mut out as *mut ffi::GhosttyKittyImageFormat).cast(),
        )
        .into_result()?;
    }
    match out {
        ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGB => Ok(KittyImageFormat::Rgb),
        ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGBA => Ok(KittyImageFormat::Rgba),
        ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_PNG => Ok(KittyImageFormat::Png),
        _ => Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE)),
    }
}

fn kitty_image_compression(
    image: ffi::GhosttyKittyGraphicsImage,
) -> Result<ffi::GhosttyKittyImageCompression, Error> {
    let mut out = ffi::GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE;
    unsafe {
        ffi::ghostty_kitty_graphics_image_get(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION,
            (&mut out as *mut ffi::GhosttyKittyImageCompression).cast(),
        )
        .into_result()?;
    }
    Ok(out)
}

fn kitty_image_data_ptr_len(
    image: ffi::GhosttyKittyGraphicsImage,
) -> Result<(*const u8, usize), Error> {
    let mut ptr_out: *const u8 = ptr::null();
    let mut len = 0usize;
    unsafe {
        ffi::ghostty_kitty_graphics_image_get(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR,
            (&mut ptr_out as *mut *const u8).cast(),
        )
        .into_result()?;
        ffi::ghostty_kitty_graphics_image_get(
            image,
            ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN,
            (&mut len as *mut usize).cast(),
        )
        .into_result()?;
    }
    Ok((ptr_out, len))
}

fn kitty_image_data_from_ptr(ptr_out: *const u8, len: usize) -> Vec<u8> {
    if ptr_out.is_null() || len == 0 {
        return Vec::new();
    }
    unsafe { slice::from_raw_parts(ptr_out, len) }.to_vec()
}

// Hashes the full payload. Callers cache the result per image id and only
// recompute it when the image's transmit time changes.
fn kitty_image_fingerprint(
    ptr_out: *const u8,
    len: usize,
    image_width: u32,
    image_height: u32,
    format: KittyImageFormat,
) -> u64 {
    let mut hasher = DefaultHasher::new();
    len.hash(&mut hasher);
    image_width.hash(&mut hasher);
    image_height.hash(&mut hasher);
    format.hash(&mut hasher);
    if ptr_out.is_null() || len == 0 {
        return hasher.finish();
    }

    let data = unsafe { slice::from_raw_parts(ptr_out, len) };
    data.hash(&mut hasher);
    hasher.finish()
}

fn grid_ref_hyperlink_uri(grid_ref: &ffi::GhosttyGridRef) -> Result<Option<String>, Error> {
    let mut required = 0usize;
    let result =
        unsafe { ffi::ghostty_grid_ref_hyperlink_uri(grid_ref, ptr::null_mut(), 0, &mut required) };
    if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
        result.into_result()?;
    }
    if required == 0 {
        return Ok(None);
    }
    let mut buffer = vec![0u8; required];
    unsafe {
        ffi::ghostty_grid_ref_hyperlink_uri(
            grid_ref,
            buffer.as_mut_ptr(),
            buffer.len(),
            &mut required,
        )
        .into_result()?;
    }
    buffer.truncate(required);
    Ok(Some(String::from_utf8_lossy(&buffer).into_owned()))
}

pub struct RenderState {
    raw: ffi::GhosttyRenderState,
}

impl RenderState {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        // SAFETY: valid out pointer and null allocator use default allocator.
        unsafe {
            ffi::ghostty_render_state_new(ptr::null(), &mut raw).into_result()?;
        }
        Ok(Self { raw })
    }

    pub fn update(&mut self, terminal: &Terminal) -> Result<(), Error> {
        // SAFETY: both handles are valid for the duration of the call.
        unsafe { ffi::ghostty_render_state_update(self.raw, terminal.raw()).into_result() }
    }

    pub fn cols(&self) -> Result<u16, Error> {
        self.get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLS)
    }

    pub fn rows(&self) -> Result<u16, Error> {
        self.get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROWS)
    }

    pub fn dirty(&self) -> Result<Dirty, Error> {
        let mut out = ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE;
        // SAFETY: out points to the matching enum storage for the requested data kind.
        unsafe {
            ffi::ghostty_render_state_get(
                self.raw,
                ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_DIRTY,
                (&mut out as *mut ffi::GhosttyRenderStateDirty).cast(),
            )
            .into_result()?;
        }
        Ok(Dirty::from_raw(out))
    }

    pub fn cursor_visible(&self) -> Result<bool, Error> {
        self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE)
    }

    pub fn cursor_blinking(&self) -> Result<bool, Error> {
        self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING)
    }

    pub fn cursor_visual_style(&self) -> Result<CursorVisualStyle, Error> {
        let mut out: ffi::GhosttyRenderStateCursorVisualStyle = 0;
        // SAFETY: out points to the matching enum storage for the requested data kind.
        unsafe {
            ffi::ghostty_render_state_get(
                self.raw,
                ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE,
                (&mut out as *mut ffi::GhosttyRenderStateCursorVisualStyle).cast(),
            )
            .into_result()?;
        }
        Ok(CursorVisualStyle::from_raw(out))
    }

    pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>, Error> {
        if !self.get_bool(
            ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
        )? {
            return Ok(None);
        }
        Ok(Some(CursorViewport {
            x: self
                .get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X)?,
            y: self
                .get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y)?,
            wide_tail: self.get_bool(
                ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
            )?,
        }))
    }

    pub fn colors(&self) -> Result<RenderColors, Error> {
        let mut colors = ffi::GhosttyRenderStateColors {
            size: mem::size_of::<ffi::GhosttyRenderStateColors>(),
            ..Default::default()
        };
        unsafe {
            ffi::ghostty_render_state_colors_get(self.raw, &mut colors).into_result()?;
        }
        Ok(RenderColors {
            background: colors.background.into(),
            foreground: colors.foreground.into(),
            palette: colors.palette.map(Into::into),
        })
    }

    pub fn set_dirty(&mut self, dirty: Dirty) -> Result<(), Error> {
        let value = dirty.as_raw();
        // SAFETY: value pointer matches the expected option type.
        unsafe {
            ffi::ghostty_render_state_set(
                self.raw,
                ffi::GhosttyRenderStateOption_GHOSTTY_RENDER_STATE_OPTION_DIRTY,
                (&value as *const ffi::GhosttyRenderStateDirty).cast(),
            )
            .into_result()
        }
    }

    pub fn populate_row_iterator<'a>(
        &'a self,
        iterator: &'a mut RowIterator,
    ) -> Result<RowIter<'a>, Error> {
        // SAFETY: iterator raw handle is valid and will not outlive self.
        unsafe {
            ffi::ghostty_render_state_get(
                self.raw,
                ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR,
                (&mut iterator.raw as *mut ffi::GhosttyRenderStateRowIterator).cast(),
            )
            .into_result()?;
        }
        Ok(RowIter {
            iterator,
            _state: PhantomData,
        })
    }

    fn get_u16(&self, data: ffi::GhosttyRenderStateData) -> Result<u16, Error> {
        let mut out = 0u16;
        // SAFETY: out points to a u16 matching the requested render-state data type.
        unsafe {
            ffi::ghostty_render_state_get(self.raw, data, (&mut out as *mut u16).cast())
                .into_result()?;
        }
        Ok(out)
    }

    fn get_bool(&self, data: ffi::GhosttyRenderStateData) -> Result<bool, Error> {
        let mut out = false;
        unsafe {
            ffi::ghostty_render_state_get(self.raw, data, (&mut out as *mut bool).cast())
                .into_result()?;
        }
        Ok(out)
    }
}

// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
unsafe impl Send for RenderState {}

impl Drop for RenderState {
    fn drop(&mut self) {
        // SAFETY: freeing a null or live handle is allowed by the C API.
        unsafe {
            ffi::ghostty_render_state_free(self.raw);
        }
    }
}

pub struct KeyEvent {
    raw: ffi::GhosttyKeyEvent,
}

impl KeyEvent {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        unsafe { ffi::ghostty_key_event_new(ptr::null(), &mut raw).into_result()? };
        Ok(Self { raw })
    }

    pub fn set_action(&mut self, action: ffi::GhosttyKeyAction) {
        unsafe { ffi::ghostty_key_event_set_action(self.raw, action) }
    }

    pub fn set_key(&mut self, key: u32) {
        unsafe { ffi::ghostty_key_event_set_key(self.raw, key) }
    }

    pub fn set_mods(&mut self, mods: u16) {
        unsafe { ffi::ghostty_key_event_set_mods(self.raw, mods) }
    }

    pub fn set_utf8(&mut self, text: &str) {
        unsafe {
            ffi::ghostty_key_event_set_utf8(self.raw, text.as_ptr().cast::<c_char>(), text.len())
        }
    }

    pub fn set_unshifted_codepoint(&mut self, codepoint: u32) {
        unsafe { ffi::ghostty_key_event_set_unshifted_codepoint(self.raw, codepoint) }
    }
}

impl Drop for KeyEvent {
    fn drop(&mut self) {
        unsafe { ffi::ghostty_key_event_free(self.raw) }
    }
}

pub struct KeyEncoder {
    raw: ffi::GhosttyKeyEncoder,
}

impl KeyEncoder {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        unsafe { ffi::ghostty_key_encoder_new(ptr::null(), &mut raw).into_result()? };
        Ok(Self { raw })
    }

    pub fn set_from_terminal(&mut self, terminal: &Terminal) {
        unsafe { ffi::ghostty_key_encoder_setopt_from_terminal(self.raw, terminal.raw()) }
    }

    pub fn encode(&mut self, event: &KeyEvent) -> Result<Vec<u8>, Error> {
        encode_with_retry(|buf, len, out_len| unsafe {
            ffi::ghostty_key_encoder_encode(self.raw, event.raw, buf, len, out_len)
        })
    }
}

// SAFETY: the opaque encoder handle is only used behind external synchronization in pane runtime.
unsafe impl Send for KeyEncoder {}

impl Drop for KeyEncoder {
    fn drop(&mut self) {
        unsafe { ffi::ghostty_key_encoder_free(self.raw) }
    }
}

pub struct MouseEvent {
    raw: ffi::GhosttyMouseEvent,
}

impl MouseEvent {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        unsafe { ffi::ghostty_mouse_event_new(ptr::null(), &mut raw).into_result()? };
        Ok(Self { raw })
    }

    pub fn set_action(&mut self, action: ffi::GhosttyMouseAction) {
        unsafe { ffi::ghostty_mouse_event_set_action(self.raw, action) }
    }

    pub fn set_button(&mut self, button: ffi::GhosttyMouseButton) {
        unsafe { ffi::ghostty_mouse_event_set_button(self.raw, button) }
    }

    pub fn clear_button(&mut self) {
        unsafe { ffi::ghostty_mouse_event_clear_button(self.raw) }
    }

    pub fn set_mods(&mut self, mods: u16) {
        unsafe { ffi::ghostty_mouse_event_set_mods(self.raw, mods) }
    }

    pub fn set_position(&mut self, x: f32, y: f32) {
        unsafe {
            ffi::ghostty_mouse_event_set_position(self.raw, ffi::GhosttyMousePosition { x, y })
        }
    }
}

impl Drop for MouseEvent {
    fn drop(&mut self) {
        unsafe { ffi::ghostty_mouse_event_free(self.raw) }
    }
}

pub struct MouseEncoder {
    raw: ffi::GhosttyMouseEncoder,
}

impl MouseEncoder {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        unsafe { ffi::ghostty_mouse_encoder_new(ptr::null(), &mut raw).into_result()? };
        Ok(Self { raw })
    }

    pub fn set_from_terminal(&mut self, terminal: &Terminal) {
        unsafe { ffi::ghostty_mouse_encoder_setopt_from_terminal(self.raw, terminal.raw()) }
    }

    pub fn set_size(
        &mut self,
        screen_width: u32,
        screen_height: u32,
        cell_width: u32,
        cell_height: u32,
    ) {
        let size = ffi::GhosttyMouseEncoderSize {
            size: std::mem::size_of::<ffi::GhosttyMouseEncoderSize>(),
            screen_width,
            screen_height,
            cell_width,
            cell_height,
            padding_top: 0,
            padding_bottom: 0,
            padding_right: 0,
            padding_left: 0,
        };
        unsafe {
            ffi::ghostty_mouse_encoder_setopt(
                self.raw,
                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_SIZE,
                (&size as *const ffi::GhosttyMouseEncoderSize).cast(),
            )
        }
    }

    pub fn set_format(&mut self, format: ffi::GhosttyMouseFormat) {
        unsafe {
            ffi::ghostty_mouse_encoder_setopt(
                self.raw,
                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_FORMAT,
                (&format as *const ffi::GhosttyMouseFormat).cast(),
            )
        }
    }

    pub fn encode(&mut self, event: &MouseEvent) -> Result<Vec<u8>, Error> {
        encode_with_retry(|buf, len, out_len| unsafe {
            ffi::ghostty_mouse_encoder_encode(self.raw, event.raw, buf, len, out_len)
        })
    }
}

impl Drop for MouseEncoder {
    fn drop(&mut self) {
        unsafe { ffi::ghostty_mouse_encoder_free(self.raw) }
    }
}

fn encode_with_retry(
    mut encode: impl FnMut(*mut c_char, usize, *mut usize) -> ffi::GhosttyResult,
) -> Result<Vec<u8>, Error> {
    let mut required = 0usize;
    let result = encode(ptr::null_mut(), 0, &mut required);
    if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
        result.into_result()?;
    }
    let mut buffer = vec![0u8; required.max(16)];
    let mut written = 0usize;
    encode(
        buffer.as_mut_ptr().cast::<c_char>(),
        buffer.len(),
        &mut written,
    )
    .into_result()?;
    buffer.truncate(written);
    Ok(buffer)
}

pub struct RowIterator {
    raw: ffi::GhosttyRenderStateRowIterator,
}

impl RowIterator {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        // SAFETY: valid out pointer and null allocator use default allocator.
        unsafe {
            ffi::ghostty_render_state_row_iterator_new(ptr::null(), &mut raw).into_result()?;
        }
        Ok(Self { raw })
    }
}

// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
unsafe impl Send for RowIterator {}

impl Drop for RowIterator {
    fn drop(&mut self) {
        // SAFETY: freeing a null or live handle is allowed by the C API.
        unsafe {
            ffi::ghostty_render_state_row_iterator_free(self.raw);
        }
    }
}

pub struct RowIter<'a> {
    iterator: &'a mut RowIterator,
    _state: PhantomData<&'a RenderState>,
}

impl<'a> RowIter<'a> {
    pub fn next(&mut self) -> bool {
        // SAFETY: iterator handle is valid while self is alive.
        unsafe { ffi::ghostty_render_state_row_iterator_next(self.iterator.raw) }
    }

    pub fn dirty(&self) -> Result<bool, Error> {
        let mut dirty = false;
        // SAFETY: dirty output matches requested row data type.
        unsafe {
            ffi::ghostty_render_state_row_get(
                self.iterator.raw,
                ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY,
                (&mut dirty as *mut bool).cast(),
            )
            .into_result()?;
        }
        Ok(dirty)
    }

    #[cfg(windows)]
    pub fn wrap_state(&self) -> Result<(bool, bool), Error> {
        let mut row = 0;
        // SAFETY: row output matches requested row data type.
        unsafe {
            ffi::ghostty_render_state_row_get(
                self.iterator.raw,
                ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_RAW,
                (&mut row as *mut ffi::GhosttyRow).cast(),
            )
            .into_result()?;
        }
        let mut soft_wrapped = false;
        // SAFETY: wrap output matches requested row data type.
        unsafe {
            ffi::ghostty_row_get(
                row,
                ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP,
                (&mut soft_wrapped as *mut bool).cast(),
            )
            .into_result()?;
        }
        let mut wrap_continuation = false;
        // SAFETY: wrap continuation output matches requested row data type.
        unsafe {
            ffi::ghostty_row_get(
                row,
                ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP_CONTINUATION,
                (&mut wrap_continuation as *mut bool).cast(),
            )
            .into_result()?;
        }
        Ok((soft_wrapped, wrap_continuation))
    }

    pub fn clear_dirty(&mut self) -> Result<(), Error> {
        self.set_dirty(false)
    }

    pub fn set_dirty(&mut self, dirty: bool) -> Result<(), Error> {
        // SAFETY: dirty pointer matches the expected row option type.
        unsafe {
            ffi::ghostty_render_state_row_set(
                self.iterator.raw,
                ffi::GhosttyRenderStateRowOption_GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY,
                (&dirty as *const bool).cast(),
            )
            .into_result()
        }
    }

    pub fn selection(&self) -> Result<Option<RowSelection>, Error> {
        let mut selection = ffi::GhosttyRenderStateRowSelection {
            size: mem::size_of::<ffi::GhosttyRenderStateRowSelection>(),
            ..Default::default()
        };
        let result = unsafe {
            ffi::ghostty_render_state_row_get(
                self.iterator.raw,
                ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION,
                (&mut selection as *mut ffi::GhosttyRenderStateRowSelection).cast(),
            )
        };
        match result {
            ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(RowSelection {
                start_x: selection.start_x,
                end_x: selection.end_x,
            })),
            ffi::GhosttyResult_GHOSTTY_NO_VALUE => Ok(None),
            other => Err(Error(other)),
        }
    }

    pub fn populate_cells<'b>(
        &'b mut self,
        cells: &'b mut RowCells,
    ) -> Result<RowCellIter<'b>, Error> {
        // SAFETY: cells raw handle is valid and will not outlive the current row borrow.
        unsafe {
            ffi::ghostty_render_state_row_get(
                self.iterator.raw,
                ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_CELLS,
                (&mut cells.raw as *mut ffi::GhosttyRenderStateRowCells).cast(),
            )
            .into_result()?;
        }
        Ok(RowCellIter { cells })
    }
}

pub struct RowCells {
    raw: ffi::GhosttyRenderStateRowCells,
}

impl RowCells {
    pub fn new() -> Result<Self, Error> {
        let mut raw = ptr::null_mut();
        // SAFETY: valid out pointer and null allocator use default allocator.
        unsafe {
            ffi::ghostty_render_state_row_cells_new(ptr::null(), &mut raw).into_result()?;
        }
        Ok(Self { raw })
    }
}

// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
unsafe impl Send for RowCells {}

impl Drop for RowCells {
    fn drop(&mut self) {
        // SAFETY: freeing a null or live handle is allowed by the C API.
        unsafe {
            ffi::ghostty_render_state_row_cells_free(self.raw);
        }
    }
}

pub struct RowCellIter<'a> {
    cells: &'a mut RowCells,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CellBasicData {
    pub wide: CellWide,
    pub has_hyperlink: bool,
    pub has_styling: bool,
    pub style: CellStyle,
}

impl Default for CellBasicData {
    fn default() -> Self {
        Self {
            wide: CellWide::Narrow,
            has_hyperlink: false,
            has_styling: false,
            style: CellStyle::default(),
        }
    }
}

impl<'a> RowCellIter<'a> {
    pub fn next(&mut self) -> bool {
        // SAFETY: cells handle is valid while self is alive.
        unsafe { ffi::ghostty_render_state_row_cells_next(self.cells.raw) }
    }

    pub fn select(&mut self, x: u16) -> Result<(), Error> {
        unsafe { ffi::ghostty_render_state_row_cells_select(self.cells.raw, x).into_result() }
    }

    fn raw_cell(&self) -> Result<ffi::GhosttyCell, Error> {
        let mut raw = ffi::GhosttyCell::default();
        unsafe {
            ffi::ghostty_render_state_row_cells_get(
                self.cells.raw,
                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW,
                (&mut raw as *mut ffi::GhosttyCell).cast(),
            )
            .into_result()?;
        }
        Ok(raw)
    }

    pub fn basic_data(&self) -> Result<CellBasicData, Error> {
        let mut raw = ffi::GhosttyCell::default();
        let mut style = ffi::GhosttyStyle {
            size: mem::size_of::<ffi::GhosttyStyle>(),
            ..Default::default()
        };
        let mut has_styling = false;
        let row_keys = [
            ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW,
            ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
            ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING,
        ];
        let mut row_values = [
            (&mut raw as *mut ffi::GhosttyCell).cast::<c_void>(),
            (&mut style as *mut ffi::GhosttyStyle).cast::<c_void>(),
            (&mut has_styling as *mut bool).cast::<c_void>(),
        ];
        let mut written = 0usize;
        unsafe {
            ffi::ghostty_render_state_row_cells_get_multi(
                self.cells.raw,
                row_keys.len(),
                row_keys.as_ptr(),
                row_values.as_mut_ptr(),
                &mut written,
            )
            .into_result()?;
        }

        let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW;
        let mut has_hyperlink = false;
        let cell_keys = [
            ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE,
            ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_HYPERLINK,
        ];
        let mut cell_values = [
            (&mut wide as *mut ffi::GhosttyCellWide).cast::<c_void>(),
            (&mut has_hyperlink as *mut bool).cast::<c_void>(),
        ];
        unsafe {
            ffi::ghostty_cell_get_multi(
                raw,
                cell_keys.len(),
                cell_keys.as_ptr(),
                cell_values.as_mut_ptr(),
                &mut written,
            )
            .into_result()?;
        }

        Ok(CellBasicData {
            wide: CellWide::from_raw(wide),
            has_hyperlink,
            has_styling,
            style: style.into(),
        })
    }

    pub fn wide(&self) -> Result<CellWide, Error> {
        let raw = self.raw_cell()?;
        let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW;
        unsafe {
            ffi::ghostty_cell_get(
                raw,
                ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE,
                (&mut wide as *mut ffi::GhosttyCellWide).cast(),
            )
            .into_result()?;
        }
        Ok(CellWide::from_raw(wide))
    }

    pub fn has_hyperlink(&self) -> Result<bool, Error> {
        let raw = self.raw_cell()?;
        let mut has_hyperlink = false;
        unsafe {
            ffi::ghostty_cell_get(
                raw,
                ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_HYPERLINK,
                (&mut has_hyperlink as *mut bool).cast(),
            )
            .into_result()?;
        }
        Ok(has_hyperlink)
    }

    pub fn style(&self) -> Result<CellStyle, Error> {
        let mut style = ffi::GhosttyStyle {
            size: mem::size_of::<ffi::GhosttyStyle>(),
            ..Default::default()
        };
        unsafe {
            ffi::ghostty_render_state_row_cells_get(
                self.cells.raw,
                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
                (&mut style as *mut ffi::GhosttyStyle).cast(),
            )
            .into_result()?;
        }
        Ok(style.into())
    }

    pub fn content_bg_color(&self) -> Result<Option<CellColor>, Error> {
        let raw = self.raw_cell()?;
        let mut tag = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_CODEPOINT;
        unsafe {
            ffi::ghostty_cell_get(
                raw,
                ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CONTENT_TAG,
                (&mut tag as *mut ffi::GhosttyCellContentTag).cast(),
            )
            .into_result()?;
        }

        match tag {
            ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE => {
                let mut index = 0u8;
                unsafe {
                    ffi::ghostty_cell_get(
                        raw,
                        ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_PALETTE,
                        (&mut index as *mut u8).cast(),
                    )
                    .into_result()?;
                }
                Ok(Some(CellColor::Palette(index)))
            }
            ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_RGB => {
                let mut color = ffi::GhosttyColorRgb::default();
                unsafe {
                    ffi::ghostty_cell_get(
                        raw,
                        ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_RGB,
                        (&mut color as *mut ffi::GhosttyColorRgb).cast(),
                    )
                    .into_result()?;
                }
                Ok(Some(CellColor::Rgb(color.into())))
            }
            _ => Ok(None),
        }
    }

    pub fn fg_color(&self) -> Result<Option<RgbColor>, Error> {
        let mut color = ffi::GhosttyColorRgb::default();
        let result = unsafe {
            ffi::ghostty_render_state_row_cells_get(
                self.cells.raw,
                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR,
                (&mut color as *mut ffi::GhosttyColorRgb).cast(),
            )
        };
        match result {
            ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(color.into())),
            ffi::GhosttyResult_GHOSTTY_INVALID_VALUE => Ok(None),
            other => Err(Error(other)),
        }
    }

    pub fn bg_color(&self) -> Result<Option<RgbColor>, Error> {
        let mut color = ffi::GhosttyColorRgb::default();
        let result = unsafe {
            ffi::ghostty_render_state_row_cells_get(
                self.cells.raw,
                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR,
                (&mut color as *mut ffi::GhosttyColorRgb).cast(),
            )
        };
        match result {
            ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(color.into())),
            ffi::GhosttyResult_GHOSTTY_INVALID_VALUE => Ok(None),
            other => Err(Error(other)),
        }
    }

    fn raw_cell_text_into(&self, text: &mut String) -> Result<(), Error> {
        let raw = self.raw_cell()?;
        let mut has_text = false;
        unsafe {
            ffi::ghostty_cell_get(
                raw,
                ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_TEXT,
                (&mut has_text as *mut bool).cast(),
            )
            .into_result()?;
        }
        if !has_text {
            return Ok(());
        }

        let mut codepoint = 0u32;
        unsafe {
            ffi::ghostty_cell_get(
                raw,
                ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CODEPOINT,
                (&mut codepoint as *mut u32).cast(),
            )
            .into_result()?;
        }
        if let Some(ch) = char::from_u32(codepoint) {
            text.push(ch);
        }
        Ok(())
    }

    pub fn grapheme_text(&self) -> Result<String, Error> {
        let mut bytes = Vec::new();
        let mut text = String::new();
        self.grapheme_text_into(&mut bytes, &mut text)?;
        Ok(text)
    }

    pub fn grapheme_text_into(&self, bytes: &mut Vec<u8>, text: &mut String) -> Result<(), Error> {
        text.clear();
        bytes.clear();

        let mut buffer = ffi::GhosttyBuffer {
            ptr: ptr::null_mut(),
            cap: 0,
            len: 0,
        };
        let result = unsafe {
            ffi::ghostty_render_state_row_cells_get(
                self.cells.raw,
                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8,
                (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
            )
        };
        match result {
            ffi::GhosttyResult_GHOSTTY_SUCCESS if buffer.len == 0 => {
                return self.raw_cell_text_into(text);
            }
            ffi::GhosttyResult_GHOSTTY_SUCCESS => {
                return Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE));
            }
            ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE => {}
            other => return Err(Error(other)),
        }

        if buffer.len == 0 {
            return self.raw_cell_text_into(text);
        }
        bytes.resize(buffer.len, 0);
        let mut buffer = ffi::GhosttyBuffer {
            ptr: bytes.as_mut_ptr(),
            cap: bytes.len(),
            len: 0,
        };
        unsafe {
            ffi::ghostty_render_state_row_cells_get(
                self.cells.raw,
                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8,
                (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
            )
            .into_result()?;
        }
        if buffer.len > bytes.len() {
            return Err(Error(ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE));
        }
        bytes.truncate(buffer.len);
        match std::str::from_utf8(bytes) {
            Ok(value) => text.push_str(value),
            Err(_) => text.push_str(&String::from_utf8_lossy(bytes)),
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn write_numbered_lines(terminal: &mut Terminal, count: usize) {
        for i in 0..count {
            terminal.write(format!("{i:06}\r\n").as_bytes());
        }
    }

    fn write_padded_lines(terminal: &mut Terminal, count: usize, width: usize) {
        let line = format!("{}\r\n", "x".repeat(width));
        terminal.write(line.repeat(count).as_bytes());
    }

    fn first_rendered_row_text(terminal: &Terminal) -> String {
        let mut render_state = RenderState::new().unwrap();
        render_state.update(terminal).unwrap();
        let mut row_iterator = RowIterator::new().unwrap();
        let mut rows = render_state
            .populate_row_iterator(&mut row_iterator)
            .unwrap();
        let mut row_cells = RowCells::new().unwrap();
        let mut bytes = Vec::new();
        let mut cell_text = String::new();
        let mut row_text = String::new();

        assert!(rows.next());
        let mut cells = rows.populate_cells(&mut row_cells).unwrap();
        while cells.next() {
            cells
                .grapheme_text_into(&mut bytes, &mut cell_text)
                .unwrap();
            row_text.push_str(&cell_text);
        }
        row_text.trim_end().to_owned()
    }

    fn build_info_bool(data: ffi::GhosttyBuildInfo) -> bool {
        let mut out = false;
        unsafe {
            ffi::ghostty_build_info(data, (&mut out as *mut bool).cast())
                .into_result()
                .unwrap();
        }
        out
    }

    fn build_info_optimize() -> ffi::GhosttyOptimizeMode {
        let mut out = ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_DEBUG;
        unsafe {
            ffi::ghostty_build_info(
                ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_OPTIMIZE,
                (&mut out as *mut ffi::GhosttyOptimizeMode).cast(),
            )
            .into_result()
            .unwrap();
        }
        out
    }

    #[test]
    fn kitty_image_fingerprint_covers_full_payload() {
        let mut data = vec![1u8; 4096 * 4];
        let original =
            kitty_image_fingerprint(data.as_ptr(), data.len(), 100, 50, KittyImageFormat::Png);

        data[4096 + 123] = 2;
        let changed_outside_sampled_windows =
            kitty_image_fingerprint(data.as_ptr(), data.len(), 100, 50, KittyImageFormat::Png);
        assert_ne!(original, changed_outside_sampled_windows);
    }

    #[test]
    fn kitty_image_fingerprint_refreshes_on_retransmission() {
        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.write(b"\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=10,r=5,q=2;/wAA/w==\x1b\\");
        let first = terminal
            .kitty_image_placements_with_data_filter(|_| true)
            .unwrap();
        assert_eq!(first.len(), 1);
        let first_generation = terminal
            .kitty_fingerprints
            .lock()
            .unwrap()
            .get(&7)
            .unwrap()
            .generation;
        assert_ne!(first_generation, 0);

        // Same id and size, different pixels.
        terminal.write(b"\x1b_Ga=t,f=32,t=d,i=7,s=1,v=1,q=2;AAAAAA==\x1b\\");
        let second = terminal
            .kitty_image_placements_with_data_filter(|_| true)
            .unwrap();
        assert_eq!(second.len(), 1);
        assert_ne!(first[0].data_fingerprint, second[0].data_fingerprint);
        let second_generation = terminal
            .kitty_fingerprints
            .lock()
            .unwrap()
            .get(&7)
            .unwrap()
            .generation;
        assert_ne!(first_generation, second_generation);

        // No retransmission, so the fingerprint and generation stay stable.
        let third = terminal
            .kitty_image_placements_with_data_filter(|_| true)
            .unwrap();
        assert_eq!(second[0].data_fingerprint, third[0].data_fingerprint);
        assert_eq!(
            terminal
                .kitty_fingerprints
                .lock()
                .unwrap()
                .get(&7)
                .unwrap()
                .generation,
            second_generation
        );
    }

    #[test]
    fn kitty_storage_generation_skips_only_proven_empty_storage() {
        let mut terminal = Terminal::new(10, 5, 1_000_000).unwrap();
        terminal.enable_kitty_graphics().unwrap();
        terminal.resize(10, 5, 8, 16).unwrap();

        assert_eq!(terminal.kitty_graphics_generation().unwrap(), 0);
        assert!(terminal.kitty_image_placements().unwrap().is_empty());

        terminal.write(b"\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\");
        let transmitted = terminal.kitty_graphics_generation().unwrap();
        assert_ne!(transmitted, 0);
        assert!(terminal.kitty_image_placements().unwrap().is_empty());
        assert_eq!(terminal.kitty_empty_generation.get(), Some(transmitted));

        terminal.write(b"plain text");
        assert_eq!(terminal.kitty_graphics_generation().unwrap(), transmitted);
        assert!(terminal.kitty_image_placements().unwrap().is_empty());

        terminal.write(b"\x1b_Ga=p,i=1,p=1,c=1,r=1;\x1b\\");
        let placed = terminal.kitty_graphics_generation().unwrap();
        assert_ne!(placed, transmitted);
        assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);

        terminal.resize(10, 5, 12, 24).unwrap();
        assert_eq!(terminal.kitty_graphics_generation().unwrap(), placed);
        assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);

        write_numbered_lines(&mut terminal, 20);
        assert_eq!(terminal.kitty_graphics_generation().unwrap(), placed);
        assert!(terminal.kitty_image_placements().unwrap().is_empty());
        assert_ne!(terminal.kitty_empty_generation.get(), Some(placed));
        terminal.scroll_viewport_row(0);
        assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);

        terminal.write(b"\x1b_Ga=d,d=A\x1b\\");
        let deleted = terminal.kitty_graphics_generation().unwrap();
        assert_ne!(deleted, placed);
        assert!(terminal.kitty_image_placements().unwrap().is_empty());
        assert_eq!(terminal.kitty_empty_generation.get(), Some(deleted));
    }

    #[test]
    fn build_info_contract_matches_expected_vendored_features() {
        let _simd = build_info_bool(ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_SIMD);
        let _tmux_control_mode =
            build_info_bool(ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE);
        let _kitty_graphics =
            build_info_bool(ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_KITTY_GRAPHICS);

        let optimize = build_info_optimize();
        assert!(matches!(
            optimize,
            ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_DEBUG
                | ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_RELEASE_SAFE
                | ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_RELEASE_SMALL
                | ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_RELEASE_FAST
        ));
    }

    #[test]
    fn kitty_graphics_direct_rgba_placement_is_queryable() {
        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.enable_kitty_graphics().unwrap();
        terminal.resize(10, 5, 8, 16).unwrap();
        terminal.write(b"\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=10,r=5,q=2;/wAA/w==\x1b\\");

        let placements = terminal.kitty_image_placements().unwrap();
        assert_eq!(placements.len(), 1);
        assert_eq!(placements[0].image_id, 7);
        assert_eq!(placements[0].placement_id, 3);
        assert_eq!(placements[0].image_width, 1);
        assert_eq!(placements[0].image_height, 1);
        assert_eq!(placements[0].format, KittyImageFormat::Rgba);
        assert_eq!(placements[0].data, [255, 0, 0, 255]);
        assert_eq!(placements[0].render.grid_cols, 10);
        assert_eq!(placements[0].render.grid_rows, 5);
    }

    #[test]
    fn kitty_graphics_local_media_are_enabled() {
        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.enable_kitty_graphics().unwrap();

        assert!(terminal
            .get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE)
            .unwrap());
        assert!(terminal
            .get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE)
            .unwrap());
        assert!(terminal
            .get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM)
            .unwrap());
    }

    #[test]
    fn kitty_graphics_file_medium_rgba_placement_is_queryable() {
        use base64::Engine;

        let dir = std::env::temp_dir().join(format!(
            "herdr-kitty-file-medium-test-{}",
            std::process::id()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("pixel.rgba");
        std::fs::write(&path, [255, 0, 0, 255]).unwrap();

        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.enable_kitty_graphics().unwrap();
        terminal.resize(10, 5, 8, 16).unwrap();
        let encoded_path =
            base64::engine::general_purpose::STANDARD.encode(path.as_os_str().as_encoded_bytes());
        let command =
            format!("\x1b_Ga=T,f=32,t=f,i=9,p=4,s=1,v=1,c=10,r=5,q=2;{encoded_path}\x1b\\");
        terminal.write(command.as_bytes());
        terminal.write(b"\x1b_Ga=p,U=1,i=9,c=10,r=5\x1b\\");

        let placements = terminal.kitty_image_placements().unwrap();
        assert_eq!(placements.len(), 1);
        assert_eq!(placements[0].image_id, 9);
        assert_eq!(placements[0].placement_id, 4);
        assert_eq!(placements[0].image_width, 1);
        assert_eq!(placements[0].image_height, 1);
        assert_eq!(placements[0].format, KittyImageFormat::Rgba);
        assert_eq!(placements[0].data, [255, 0, 0, 255]);
        assert_eq!(placements[0].render.grid_cols, 10);
        assert_eq!(placements[0].render.grid_rows, 5);

        let _ = std::fs::remove_dir_all(dir);
    }

    #[cfg(unix)]
    #[test]
    fn kitty_graphics_file_upload_can_be_placed_later() {
        let dir = std::env::temp_dir().join(format!(
            "herdr-kitty-file-upload-test-{}",
            std::process::id()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("pixel.rgba");
        std::fs::write(&path, [255, 0, 0, 255]).unwrap();

        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.enable_kitty_graphics().unwrap();
        terminal.resize(10, 5, 8, 16).unwrap();
        let mut upload = Vec::new();
        crate::kitty_graphics::encode_kitty_regular_file(
            &mut upload,
            &[],
            "a=t,f=32,s=1,v=1,i=10,q=0",
            path.to_str().unwrap(),
        );
        terminal.write(&upload);
        assert!(terminal.kitty_image_placements().unwrap().is_empty());

        terminal.write(b"\x1b_Ga=p,i=10,p=5,c=10,r=5,C=1,q=2\x1b\\");
        let placements = terminal.kitty_image_placements().unwrap();
        assert_eq!(placements.len(), 1);
        assert_eq!(placements[0].image_id, 10);
        assert_eq!(placements[0].placement_id, 5);
        assert_eq!(placements[0].image_width, 1);
        assert_eq!(placements[0].image_height, 1);
        assert_eq!(placements[0].format, KittyImageFormat::Rgba);
        assert_eq!(placements[0].data, [255, 0, 0, 255]);

        let _ = std::fs::remove_dir_all(dir);
    }

    #[test]
    fn kitty_graphics_unicode_placeholder_placement_is_queryable() {
        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.enable_kitty_graphics().unwrap();
        terminal.resize(10, 5, 8, 16).unwrap();
        terminal.write(b"\x1b_Gq=2,a=t,t=d,f=32,s=1,v=1,i=1193046,m=0;/wAA/w==\x1b\\");
        terminal.write(b"\x1b_Gq=2,a=p,U=1,i=1193046,c=2,r=1\x1b\\");
        terminal.write("\x1b[2;3H\x1b[38;2;18;52;86m\u{10eeee}\u{0305}\u{0305}\u{10eeee}\u{0305}\u{030d}\x1b[0m".as_bytes());

        let placements = terminal.kitty_image_placements().unwrap();
        assert_eq!(placements.len(), 1);
        assert_eq!(placements[0].image_id, 1193046);
        assert_ne!(placements[0].placement_id, 0);
        assert_eq!(placements[0].image_width, 1);
        assert_eq!(placements[0].image_height, 1);
        assert_eq!(placements[0].format, KittyImageFormat::Rgba);
        assert_eq!(placements[0].data, [255, 0, 0, 255]);
        assert_eq!(placements[0].render.viewport_col, 2);
        assert_eq!(placements[0].render.viewport_row, 1);
        assert_eq!(placements[0].render.grid_cols, 2);
        assert_eq!(placements[0].render.grid_rows, 1);
    }

    #[test]
    fn unicode_width_helpers_match_terminal_layout_rules() {
        assert_eq!(unicode_codepoint_width('A' as u32), 1);
        assert_eq!(unicode_codepoint_width('\u{301}' as u32), 0);
        assert_eq!(unicode_codepoint_width('界' as u32), 2);
        assert_eq!(unicode_codepoint_width(0x11_0000), 1);

        let cases: &[(&[u32], usize, u8)] = &[
            (&[], 0, 0),
            (&['e' as u32, '\u{301}' as u32], 2, 1),
            (&['⚠' as u32, '\u{fe0f}' as u32], 2, 2),
            (&['⚠' as u32, '\u{fe0e}' as u32], 2, 1),
            (&['🇧' as u32, '🇷' as u32], 2, 2),
            (&['👍' as u32, '🏽' as u32], 2, 2),
            (
                &[
                    '👨' as u32,
                    '\u{200d}' as u32,
                    '👩' as u32,
                    '\u{200d}' as u32,
                    '👧' as u32,
                ],
                5,
                2,
            ),
            (&[0x11_0000, 'A' as u32], 1, 1),
        ];
        for &(codepoints, consumed, width) in cases {
            assert_eq!(unicode_grapheme_width(codepoints), (consumed, width));
        }
    }

    #[test]
    fn focus_encoding_matches_expected_sequences() {
        assert_eq!(encode_focus(FocusEvent::Gained).unwrap(), b"\x1b[I");
        assert_eq!(encode_focus(FocusEvent::Lost).unwrap(), b"\x1b[O");
    }

    #[test]
    fn terminal_callbacks_report_pty_responses_and_pwd_changes() {
        let mut terminal = Terminal::new(8, 3, 100).unwrap();
        let responses = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
        let sink = responses.clone();
        terminal
            .set_write_pty_callback(move |bytes| sink.lock().unwrap().extend_from_slice(bytes))
            .unwrap();

        terminal.write(b"\x1b[6n\x1b]7;file:///tmp/herdr\x07");

        let output = responses.lock().unwrap().clone();
        assert!(!output.is_empty());
        assert!(String::from_utf8_lossy(&output).contains("R"));
        assert_eq!(terminal.take_pwd_changes(), [b"file:///tmp/herdr".to_vec()]);
    }

    #[test]
    fn key_and_mouse_encoders_follow_terminal_state() {
        let mut terminal = Terminal::new(80, 24, 0).unwrap();
        terminal.mode_set(1, true).unwrap();
        terminal.write(b"\x1b[>1u\x1b[?1000h\x1b[?1006h");

        assert!(terminal.mode_get(1).unwrap());
        assert_eq!(terminal.kitty_keyboard_flags().unwrap(), 1);
        assert!(terminal.mouse_tracking_enabled().unwrap());

        let mut key_encoder = KeyEncoder::new().unwrap();
        key_encoder.set_from_terminal(&terminal);
        let mut key_event = KeyEvent::new().unwrap();
        key_event.set_action(ffi::GhosttyKeyAction_GHOSTTY_KEY_ACTION_PRESS);
        key_event.set_key(KEY_A);
        key_event.set_mods(MOD_CTRL | MOD_SHIFT);
        key_event.set_utf8("A");
        key_event.set_unshifted_codepoint('a' as u32);
        let encoded_key = key_encoder.encode(&key_event).unwrap();
        assert_eq!(encoded_key, b"\x1b[97;6u");

        let mut mouse_encoder = MouseEncoder::new().unwrap();
        mouse_encoder.set_from_terminal(&terminal);
        mouse_encoder.set_size(80, 24, 1, 1);
        let mut mouse_event = MouseEvent::new().unwrap();
        mouse_event.set_action(ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_PRESS);
        mouse_event.set_button(ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_LEFT);
        mouse_event.set_position(0.0, 0.0);
        let encoded_mouse = mouse_encoder.encode(&mouse_event).unwrap();
        assert_eq!(encoded_mouse, b"\x1b[<0;1;1M");
    }

    #[test]
    fn modify_other_keys_query_tracks_mode_two() {
        let mut terminal = Terminal::new(80, 24, 0).unwrap();

        assert!(!terminal.modify_other_keys_enabled().unwrap());
        terminal.write(b"\x1b[>4;2m");
        assert!(terminal.modify_other_keys_enabled().unwrap());
        terminal.write(b"\x1b[>4;0m");
        assert!(!terminal.modify_other_keys_enabled().unwrap());
    }

    #[test]
    fn terminal_read_text_viewport_unwraps_soft_wrapped_selection() {
        let mut terminal = Terminal::new(5, 3, 0).unwrap();
        terminal.write("1ABCD2EFGH3IJKL".as_bytes());

        let text = terminal.read_text_viewport((0, 1), (2, 2), false).unwrap();
        assert_eq!(text, "2EFGH3IJ");
    }

    #[test]
    fn terminal_extracts_viewport_hyperlink_uri() {
        let mut terminal = Terminal::new(20, 3, 0).unwrap();
        terminal.write(b"\x1b]8;;https://example.com\x1b\\Link\x1b]8;;\x1b\\");

        assert_eq!(
            terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(),
            Some("https://example.com")
        );
        assert_eq!(terminal.viewport_hyperlink_uri(4, 0).unwrap(), None);
    }

    #[test]
    fn terminal_read_text_viewport_handles_wide_chars() {
        let mut terminal = Terminal::new(5, 3, 0).unwrap();
        terminal.write("1A⚡".as_bytes());

        let full = terminal.read_text_viewport((0, 0), (3, 0), false).unwrap();
        assert_eq!(full, "1A⚡");

        let through_wide_head = terminal.read_text_viewport((0, 0), (2, 0), false).unwrap();
        assert_eq!(through_wide_head, "1A⚡");

        let wide_only = terminal.read_text_viewport((3, 0), (3, 0), false).unwrap();
        assert_eq!(wide_only, "⚡");
    }

    #[test]
    fn zero_max_scrollback_disables_history() {
        let mut terminal = Terminal::new(80, 3, 0).unwrap();
        write_numbered_lines(&mut terminal, 3000);
        assert_eq!(terminal.scrollback_rows().unwrap(), 0);
    }

    #[test]
    fn max_scrollback_limit_bytes_retains_more_history_for_larger_limits() {
        let mut small = Terminal::new(80, 3, 1_000_000).unwrap();
        let mut large = Terminal::new(80, 3, 10_000_000).unwrap();

        write_padded_lines(&mut small, 1_250, 70);
        write_padded_lines(&mut large, 1_250, 70);

        let small_scrollback = small.scrollback_rows().unwrap();
        let large_scrollback = large.scrollback_rows().unwrap();

        assert!(
            large_scrollback > small_scrollback,
            "expected larger byte limit to retain more history, got small={small_scrollback}, large={large_scrollback}"
        );
    }

    #[test]
    fn large_negative_scroll_delta_reaches_top_of_scrollback() {
        let mut terminal = Terminal::new(80, 3, 1_000_000).unwrap();
        write_numbered_lines(&mut terminal, 1000);

        let before = terminal.scrollbar().unwrap();
        assert!(before.total > before.len);

        terminal.scroll_viewport_bottom();
        terminal.scroll_viewport_delta(-10_000);

        let after = terminal.scrollbar().unwrap();
        assert_eq!(after.offset, 0);
        assert_eq!(after.len, before.len);
    }

    #[test]
    fn absolute_scroll_row_round_trips_and_clamps() {
        let mut terminal = Terminal::new(80, 3, 1_000_000).unwrap();
        write_numbered_lines(&mut terminal, 1000);

        let before = terminal.scrollbar().unwrap();
        let max_row = before.total.saturating_sub(before.len);
        assert!(max_row > 0);

        for row in [0, max_row / 2, max_row, usize::MAX] {
            terminal.scroll_viewport_row(row);
            let after = terminal.scrollbar().unwrap();
            assert_eq!(after.offset, row.min(max_row));
            assert_eq!(after.len, before.len);
        }
    }

    #[test]
    fn deep_scrollback_resize_preserves_unicode_and_hyperlinks() {
        use std::fmt::Write as _;

        let mut terminal = Terminal::new(20, 5, 100_000_000).unwrap();
        let mut input = String::from("\x1b]8;;https://example.com\x1b\\FIRST 🇧🇷\x1b]8;;\x1b\\\r\n");
        for line in 0..70_000 {
            writeln!(input, "{line:05} 👨‍👩‍👧").unwrap();
        }
        terminal.write(input.as_bytes());

        assert!(terminal.scrollback_rows().unwrap() > u16::MAX as usize);
        terminal.scroll_viewport_delta(-100_000);
        assert_eq!(terminal.scrollbar().unwrap().offset, 0);
        assert!(terminal
            .read_text_viewport((0, 0), (19, 0), false)
            .unwrap()
            .starts_with("FIRST 🇧🇷"));
        assert_eq!(
            terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(),
            Some("https://example.com")
        );

        terminal.resize(10, 5, 8, 16).unwrap();
        terminal.scroll_viewport_delta(-100_000);
        let metrics = terminal.scrollbar().unwrap();
        assert_eq!(metrics.offset, 0);
        assert_eq!(metrics.len, 5);
        assert!(terminal
            .read_text_viewport((0, 0), (9, 0), false)
            .unwrap()
            .starts_with("FIRST 🇧🇷"));
        assert_eq!(
            terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(),
            Some("https://example.com")
        );
    }

    #[test]
    fn active_screen_and_cursor_visibility_contract() {
        let mut terminal = Terminal::new(12, 3, 0).unwrap();
        let mut render_state = RenderState::new().unwrap();

        terminal.write(b"primary");
        assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Primary);
        assert_eq!(
            terminal.read_text_viewport((0, 0), (6, 0), false).unwrap(),
            "primary"
        );

        render_state.update(&terminal).unwrap();
        assert!(render_state.cursor_visible().unwrap());
        terminal.write(b"\x1b[?25l");
        render_state.update(&terminal).unwrap();
        assert!(!render_state.cursor_visible().unwrap());

        terminal.write(b"\x1b[?1049h\x1b[HALT");
        assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Alternate);
        assert_eq!(
            terminal.read_text_viewport((0, 0), (2, 0), false).unwrap(),
            "ALT"
        );

        terminal.write(b"\x1b[?1049l");
        assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Primary);
        assert_eq!(
            terminal.read_text_viewport((0, 0), (6, 0), false).unwrap(),
            "primary"
        );
    }

    #[test]
    fn terminal_and_render_state_smoke_test() {
        let mut terminal = Terminal::new(8, 3, 100).unwrap();
        assert_eq!(terminal.cols().unwrap(), 8);
        assert_eq!(terminal.rows().unwrap(), 3);

        terminal.write(b"hello\r\nworld");

        let mut render_state = RenderState::new().unwrap();
        render_state.update(&terminal).unwrap();
        assert_eq!(render_state.cols().unwrap(), 8);
        assert_eq!(render_state.rows().unwrap(), 3);
        assert_ne!(render_state.dirty().unwrap(), Dirty::Clean);

        let mut row_iterator = RowIterator::new().unwrap();
        let mut row_iter = render_state
            .populate_row_iterator(&mut row_iterator)
            .unwrap();
        let mut row_cells = RowCells::new().unwrap();

        let mut found_hello = false;
        let mut found_world = false;
        let mut row_index = 0usize;
        while row_iter.next() {
            let _ = row_iter.dirty().unwrap();
            let mut cells = row_iter.populate_cells(&mut row_cells).unwrap();
            let mut line = String::new();
            while cells.next() {
                let text = cells.grapheme_text().unwrap();
                if text.is_empty() {
                    line.push(' ');
                } else {
                    line.push_str(&text);
                }
            }
            let trimmed = line.trim_end().to_string();
            if row_index == 0 {
                found_hello = trimmed.starts_with("hello");
            }
            if row_index == 1 {
                found_world = trimmed.starts_with("world");
            }
            row_index += 1;
        }

        assert!(found_hello);
        assert!(found_world);

        render_state.set_dirty(Dirty::Clean).unwrap();
        assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
    }

    #[test]
    fn render_cells_preserve_issue_453_unicode_payload_exactly() {
        const PAYLOAD: &str = "README 👨‍👩‍👧‍👦 🧑‍💻 ✅ ⚡ 漢字 café é 🏳️‍🌈 🚀";
        let mut terminal = Terminal::new(80, 3, 100).unwrap();
        assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
        terminal.write(format!("{PAYLOAD}\r\n").as_bytes());

        assert_eq!(first_rendered_row_text(&terminal), PAYLOAD);
    }

    #[test]
    fn grapheme_cluster_mode_is_default_and_survives_full_reset() {
        let mut terminal = Terminal::new(80, 3, 100).unwrap();
        assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());

        terminal.write(b"\x1bc");

        assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
    }

    #[test]
    fn screen_text_rows_preserve_wrap_and_grapheme_cells() {
        let mut terminal = Terminal::new(5, 3, 100).unwrap();
        terminal.write("abcdef\r\n界e\u{301}".as_bytes());

        let rows = terminal.screen_text_rows().unwrap();

        assert_eq!(rows.len(), 3);
        assert!(rows[0].soft_wrapped);
        assert!(!rows[0].wrap_continuation);
        assert!(!rows[1].soft_wrapped);
        assert!(rows[1].wrap_continuation);
        assert!(!rows[2].wrap_continuation);
        assert_eq!(rows[2].cells[0].wide, CellWide::Wide);
        assert_eq!(rows[2].cells[0].graphemes, vec!['界' as u32]);
        assert_eq!(rows[2].cells[1].wide, CellWide::SpacerTail);
        assert_eq!(rows[2].cells[2].graphemes, vec!['e' as u32, 0x301]);
    }

    #[test]
    fn render_state_row_dirty_can_be_cleared_independently() {
        let mut terminal = Terminal::new(8, 3, 100).unwrap();
        let mut render_state = RenderState::new().unwrap();

        render_state.update(&terminal).unwrap();
        {
            let mut row_iterator = RowIterator::new().unwrap();
            let mut rows = render_state
                .populate_row_iterator(&mut row_iterator)
                .unwrap();
            while rows.next() {
                rows.clear_dirty().unwrap();
                assert!(!rows.dirty().unwrap());
            }
        }
        render_state.set_dirty(Dirty::Clean).unwrap();
        assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);

        terminal.write(b"A");
        render_state.update(&terminal).unwrap();
        assert_eq!(render_state.dirty().unwrap(), Dirty::Partial);

        let mut dirty_rows = 0usize;
        {
            let mut row_iterator = RowIterator::new().unwrap();
            let mut rows = render_state
                .populate_row_iterator(&mut row_iterator)
                .unwrap();
            while rows.next() {
                if rows.dirty().unwrap() {
                    dirty_rows += 1;
                    rows.clear_dirty().unwrap();
                    assert!(!rows.dirty().unwrap());
                }
            }
        }
        assert_eq!(dirty_rows, 1);
        assert_eq!(render_state.dirty().unwrap(), Dirty::Partial);

        render_state.set_dirty(Dirty::Clean).unwrap();
        assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
    }

    #[test]
    fn row_selection_returns_none_without_selection() {
        let terminal = Terminal::new(8, 3, 100).unwrap();
        let mut render_state = RenderState::new().unwrap();
        render_state.update(&terminal).unwrap();

        let mut row_iterator = RowIterator::new().unwrap();
        let mut rows = render_state
            .populate_row_iterator(&mut row_iterator)
            .unwrap();
        assert!(rows.next());
        assert_eq!(rows.selection().unwrap(), None);
    }

    fn test_clipboard_content(mime: &[u8], data: &[u8]) -> ffi::GhosttyClipboardContent {
        ffi::GhosttyClipboardContent {
            mime: ffi::GhosttyString {
                ptr: mime.as_ptr(),
                len: mime.len(),
            },
            data: ffi::GhosttyString {
                ptr: data.as_ptr(),
                len: data.len(),
            },
        }
    }

    fn invoke_clipboard_callback(
        terminal: &mut Terminal,
        contents: &[ffi::GhosttyClipboardContent],
        size: usize,
    ) -> ffi::GhosttyClipboardWriteResult {
        let request = ffi::GhosttyClipboardWrite {
            size,
            location: ffi::GhosttyClipboardLocation_GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
            contents: contents.as_ptr(),
            contents_len: contents.len(),
        };
        // SAFETY: the request and its borrowed content live through this call.
        unsafe {
            clipboard_write_trampoline(
                terminal.raw,
                (&mut *terminal.callback_state as *mut TerminalCallbackState).cast(),
                &request,
            )
        }
    }

    #[test]
    fn clipboard_callback_ignores_clear_and_rejects_unsupported_writes() {
        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        let full_size = std::mem::size_of::<ffi::GhosttyClipboardWrite>();
        let success = ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS;
        let unsupported =
            ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
        let invalid = ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;

        assert_eq!(
            invoke_clipboard_callback(&mut terminal, &[], full_size),
            success
        );
        assert!(terminal.take_clipboard_writes().is_empty());

        let empty = test_clipboard_content(b"text/plain", b"");
        assert_eq!(
            invoke_clipboard_callback(&mut terminal, &[empty], full_size),
            unsupported
        );
        let text = test_clipboard_content(b"text/plain", b"text");
        let image = test_clipboard_content(b"image/png", b"image");
        assert_eq!(
            invoke_clipboard_callback(&mut terminal, &[text, image], full_size),
            unsupported
        );

        let oversized = vec![b'x'; MAX_CLIPBOARD_BYTES + 1];
        let oversized = test_clipboard_content(b"text/plain", &oversized);
        assert_eq!(
            invoke_clipboard_callback(&mut terminal, &[oversized], full_size),
            invalid
        );
        assert_eq!(
            invoke_clipboard_callback(&mut terminal, &[text], full_size - 1),
            invalid
        );
        assert!(terminal.take_clipboard_writes().is_empty());
    }

    #[test]
    fn libghostty_completes_osc52_writes_for_bel_and_st_without_queries() {
        let mut terminal = Terminal::new(10, 5, 0).unwrap();
        terminal.write(b"\x1b]52;c;aGVs");
        assert!(terminal.take_clipboard_writes().is_empty());
        terminal.write(b"bG8=\x07");
        assert_eq!(terminal.take_clipboard_writes(), vec![b"hello".to_vec()]);

        terminal.write(b"\x1b]52;c;d29ybGQ=\x1b\\");
        assert_eq!(terminal.take_clipboard_writes(), vec![b"world".to_vec()]);

        terminal.write(b"\x1b]52;c;?\x07");
        assert!(terminal.take_clipboard_writes().is_empty());

        terminal.write(b"\x1b]52;c;\x07");
        assert!(terminal.take_clipboard_writes().is_empty());
    }

    #[test]
    fn row_cell_basic_data_uses_batched_vendor_reads() {
        let mut terminal = Terminal::new(8, 3, 100).unwrap();
        terminal.write(b"\x1b[31mA\x1b[0m");

        let mut render_state = RenderState::new().unwrap();
        render_state.update(&terminal).unwrap();

        let mut row_iterator = RowIterator::new().unwrap();
        let mut rows = render_state
            .populate_row_iterator(&mut row_iterator)
            .unwrap();
        assert!(rows.next());

        let mut row_cells = RowCells::new().unwrap();
        let mut cells = rows.populate_cells(&mut row_cells).unwrap();
        assert!(cells.next());

        let basic = cells.basic_data().unwrap();
        assert_eq!(basic.wide, CellWide::Narrow);
        assert!(basic.has_styling);
        assert_eq!(basic.style.fg_color, Some(CellColor::Palette(1)));
        assert!(!basic.has_hyperlink);
    }
}
