Ghostty Integration
RoyalTerminal uses Ghostty in two different ways: as a native VT engine with renderer interoperability, and as a public wrapper library for hosts that want direct access to the Ghostty C ABI from .NET. The second role is what RoyalTerminal.GhosttySharp exists for.
Both VT engines use RoyalTerminal's shared Avalonia/Skia host. The renderer bridge does not embed Ghostty's complete Metal/OpenGL terminal renderer, and the managed VT engine does not claim exhaustive native or standalone-application parity. The generated ABI inventory documents the pinned native type and callback bindings; regenerate it with scripts/audit-ghostty-abi.py after a dependency update.
The current dependency target is 246f702876b924a1cb7cade1e99274d1470302fc, refreshed from b40acce58 after checking upstream HEAD on October 9. Ghostling remains the reference consumer at 63842bf8e5e481160f81d348da9ff6fd27986798. The October update audit tracks native and managed delivery. The native wrapper now exposes OSC 7501, OSC 133/reset callbacks, unknown OSC capture, checksum options, pointer shape, memory usage, and compressed snapshot restoration through public upstream APIs.
The preceding native refresh includes word-selection hard-break fixes, wide-character selection, reverse-wrap correction, charset batching, packed glyph/codepoint cache keys, opt-in window resize requests, and render overscan. The shared-host implementation includes these behaviors within the current dependency APIs. The supported boundary below distinguishes delivered behavior from deliberately unsupported platform capabilities.
GhosttyRenderState exposes the new overscan/row-identity ABI: SetOverscan, requested/captured counts, signed current-row viewport Y and an opaque two-word row ID. Changing the request preserves the last completed rows; the next update captures available neighbors and requests a full redraw. Viewport height excludes overscan. Dirty iterator indices start at the highest captured row, so callers requesting overscan must use signed viewport Y for placement. IDs support equality/hash lookup only; reusing a row cache also requires a clear dirty flag. Existing callers request zero overscan and retain their viewport-only iteration contract. Native builds, the 172-type/30-callback ABI manifest and regression execution passed in the current integration validation.
The desktop action mirror also includes the appended CopyTitleToClipboard, MoveTabToNewWindow and ResizeWindow tags and the window-size union payload. The raw action binding alone does not enable application-driven resizing. RoyalTerminal also implements the separate opt-in shared-host resize policy.
Supported implementation boundary
Program status (OSC 7501)
Both engines implement the Program Status Protocol. The native engine uses Ghostty's public program-status, semantic-prompt and reset callbacks; the managed engine validates reports with the same bounds and syntax. ITerminalProgramStatusSource exposes records, change callbacks, inherited app names and process-exit cleanup. A shared store retains at most 256 records, replaces each record completely and clears subtrees by path segment. Working and blocked records expire at a new prompt or process exit; done, error and idle remain until replaced/cleared. RIS and new sessions clear everything, while DECSTR and alternate-screen changes preserve records. OSC 9;4 remains independent.
TerminalControl.ProgramStatuses publishes immutable snapshots on the UI thread with inherited names resolved. The app shows the most urgent record in the status bar and all records in its tooltip. Text is displayed as plain text, with direction controls escaped and the originating terminal identified. Reports do not trigger commands, sounds or desktop notifications. Detection replies preserve BEL/ST and contain only the fixed OSC 7501;? response.
October protocol and host additions
Both processors expose ITerminalLifecycleEffectSource for owned OSC 133 command/error bytes and ordered protocol reset effects, and ITerminalUnknownSequencePolicy for bounded unknown OSC capture. Hosts receive reports through ITerminalEffectSource.UnknownSequenceCallback; capture defaults to 4096 bytes and can be disabled with a zero limit. Known but malformed commands and canceled sequences are excluded. ITerminalChecksumPolicy enables DECRQCRA/XTCHECKSUM explicitly; checksum replies remain disabled by default.
TerminalControl.CursorClickToMove defaults to true. Unmodified prompt clicks use OSC 133's advertised click mode, with correct coordinates across history pages. Selection drags and application mouse reporting keep their existing input paths. Back/forward mouse buttons are supported through the shared host and both encoders, including UTF-8 mouse reports. X10 accepts only the three standard buttons. Shared custom shaders now receive iCursorText, iSelectionForegroundColor and iSelectionBackgroundColor as RGB uniforms.
Snapshot compression and memory accounting
Set GhosttySnapshotDecoder.SetCompressHistory(true) or ManagedTerminalSnapshotOptions.CompressHistory = true to compress restored scrollback incrementally. Compression is optional and defaults to false. Managed rows use lossless Brotli storage, retain copy-on-write ownership and restore only when their cells are read or edited. Compression does not change history quotas, snapshot formats, READY admission or the active visible viewport.
Both processors implement ITerminalMemoryUsageSource.GetMemoryUsage(). The report separates primary/alternate storage, compressed payload and Kitty images. Native reports use upstream page accounting. Managed reports estimate cell arrays and referenced text, excluding CLR headers, allocation metadata, external retained snapshots and renderer caches; these values are not process memory measurements. Querying memory or cloning row state does not inflate compressed rows.
Platform boundaries
The dependency versions are fixed for this delivery: no Avalonia or other dependency upgrade, fork, reflection into private framework state, or new third-party patch is required. The existing pinned Ghostty integration and its already-reviewed extension overlays are retained unchanged.
| Area | Supported behavior and boundary |
|---|---|
| Linux keyboard layout | Existing public key/text events are processed normally. Modified-key unshifted symbols and consumed modifiers require an explicitly supplied ITerminalKeyboardLayout; the default host does not invent them from a US layout or a later keymap state. This is the final current-version fallback, not pending framework-patch work. |
| Native Wayland notification focus | Notifications can be delivered and reported, but focus is not advertised without a supported activation API. Actual X11/XWayland XID handles retain the existing focus path. No private compositor or framework API is used. |
| Managed snapshots and graphics | Quotas, lifecycle, COW ownership and failure boundaries are covered by deterministic and native differential tests. CLR allocation/storage need not reproduce Zig's mmap/hash layout or every native out-of-memory instruction. Exact allocator identity and exhaustive failure equivalence are not acceptance criteria. |
| Rendering | Both engines use the shared Avalonia/Skia renderer, including overscan, stable row identity, fractional scrolling and image damage. Ghostty's standalone Metal/OpenGL/DisplayLink/DMA-BUF implementation is not embedded. |
| Validation | Build, ABI, parser/graphics/snapshot differentials and headless tests establish automated coverage. They do not establish interactive IME, notification permission/desktop policy, secure-input OS ownership, compositor or six-architecture runtime certification. |
The implementation is evaluated against the pinned Ghostty behavior, with Windows Terminal and xterm.js references recorded at the relevant sections. PowerShell output is preserved rather than heuristically deleting styled trailing spaces. Physical desktop/device certification remains a release-environment check, not a reason to add unsupported dependency hooks. Performance results are isolated workloads, not an exhaustive upstream-performance or whole-application speed claim.
Untrusted hyperlink dispatch
Both engines now route producer-supplied OSC 8 links through a shared safety boundary before the operating-system launcher, following Ghostty #13634 and its pinned UntrustedURL policy. Well-formed HTTP/HTTPS and nonempty mailto targets retain direct opening. Malformed, relative, remote-file and unsafe-Unicode targets cannot dispatch. Unsafe scalars are escaped visibly without normalizing web/custom target spellings; ordinary previews reuse the original string. The explicit EnableOsc8Hyperlinks switch defaults to true and does not disable plain-text web-link recognition. Hover lookup reads cells without acquiring mutable row storage. Raw link identity remains available separately from the safe preview.
The reusable control exposes ITerminalHyperlinkHost for non-direct requests; without a host they fail closed. Requests are bounded to one pending operation per control and canceled on host replacement, OSC 8 disable, detach or session stop. Host failure/cancellation never enables an unrestricted fallback. The application shell wires a passive compiled-XAML dialog and framework-free ReactiveUI view model. Custom schemes require explicit confirmation showing the escaped target and the registered application: a Launch Services bundle ID on macOS, or an application display name from GIO/Windows associations. Association lookup runs off the UI thread and never invokes a shell. If no handler name is available, the dialog explicitly identifies "the default application" and still requires confirmation, as Ghostty does. Cancel is the default action. Blocked targets remain copyable only by an explicit user action; copying uses the displayed, escaped target, following the pinned UntrustedURLAlert, not the original potentially invisible/control-bearing string. Cancellation is checked again after association lookup and after confirmation, before launching.
Local-file requests now have a separate off-thread inspector. It standardizes the path, resolves aliases, inspects the effective object and publishes only a canonical file URI for safe regular files/directories. Reserved path characters are encoded once; original URL bytes remain the protocol/activation identity. Failed inspection cannot be overridden by confirming a custom-scheme dialog. Missing leaves use their longest accessible resolved parent for the blocked preview. Remote/device paths, reserved Windows device names, alternate data streams, ambiguous trailing-dot/space components and decoded NUL paths are rejected before file access, with canonical Windows paths checked again after junction/reparse resolution. Cancellation is checked after filesystem work and again before dispatch; no filesystem result is cached across activations.
macOS uses realpath plus Core Foundation URL resource values and Launch Services UTI conformance for applications, executables and scripts, matching the pinned Swift policy's resource categories. Linux uses GIO's file kind, current-user execute permission and executable/desktop/script MIME families. It deliberately does not use GLib's broad g_content_type_can_be_executable, which includes ordinary plain text. Windows resolves an opened filesystem handle, rejects non-disk/device objects, and checks GetBinaryTypeW plus executable/script/installer/shortcut extensions appropriate to that platform. All platforms reject Ghostty's unsafe container extensions, including application directories; ordinary directory execute permission does not prohibit opening. Missing platform inspection APIs fail closed. Native resources and handles are released within the inspection operation, before its result is published.
The native-filesystem, canonical preview, safe-file dispatch, host, policy and cancellation cases pass the automated suites. Real desktop associations remain host-dependent. As with the upstream path-based OS opener, same-user filesystem or association changes can race inspection and dispatch; this is not an atomic open-by-verified-handle security boundary.
The control retains its occupied request slot until a canceled operation really finishes, including native calls that cannot be interrupted. Host replacement or OSC 8 toggling therefore cannot accumulate abandoned inspection workers. A non-cooperating custom host blocks further non-direct links on that control until it completes; allowed web/mail links retain their direct path.
File-hover previews now follow the pinned URLHoverBanner display policy through the separate ITerminalHyperlinkPathPreviewSource hook. The application wires canonical path resolution for native, managed and replay controls. It resolves file paths off the UI thread without reading file contents, MIME types or execution permissions. Scheme-less blocked targets are standardized without filesystem inspection. Web/custom URL spelling remains unchanged. Malformed, remote or otherwise rejected file targets retain escaped original previews without filesystem access, rather than hiding their rejected authority behind a local path. A preview is never cached launch permission; activation still performs fresh safety inspection against the original target.
The control coalesces producer/hover changes into one queued UI refresh and owns at most one live resolution plus the newest pending target. Epochs invalidate even A-to-B-to-A transitions coalesced before UI dispatch. Host replacement, output changes under a stationary pointer, OSC 8 disable, detach and session stop prevent late publication. Unchanged targets reuse their published value; canceled non-cooperating resolution retains its slot until completion. The read-only HoveredLinkDisplayText direct property publishes on the UI thread, outside screen locks, and the app binds it through compiled XAML to Avalonia's tooltip. Embedders may present the same property elsewhere. This uses the shared application's tooltip rather than Ghostty's SwiftUI corner banner; the display policy is the port, not standalone Ghostty window chrome. Windows Terminal's hover tooltip also follows the hovered cell, but adds canonical/Punycode spelling for IDNs; RoyalTerminal follows Ghostty's unchanged web spelling instead. xterm.js's linkifier similarly invalidates link hover when the rendered viewport changes, leaving presentation to its embedder. Both-engine headless, coordinator ordering/lifetime, native-path and allocation cases are included in automated validation; complete #13634/platform sign-off is still unverified.
For comparison, Windows Terminal filters supported/safe URIs before confirmation, and xterm.js defaults to web schemes unless an embedder supplies a link handler. RoyalTerminal follows Ghostty's scheme/Unicode policy with an application-owned decision UI, rather than treating every absolute URI as safe. Focused classifier, host, view-model, headless-dialog and both-engine click cases are included in automated validation.
Font discovery and startup
The shared Skia renderer follows Ghostty's known-family emoji lookup and background font warmup. On macOS each resolver lazily matches Apple Color Emoji once, in regular style, through its own Skia font manager. It verifies the returned family and individual glyph coverage before using the candidate. Missing/substituted families and unsupported glyphs retain general character discovery; text presentation and other platforms retain their existing paths. Whole-grapheme coverage and lazy component candidates still apply. This is a named Skia lookup, not an additional native CoreText-to-Skia bridge or a copy of Ghostty's complete font collection. Both VT engines use this renderer behavior.
Font selection also follows the pinned Ghostty configured-face presentation rules and adjacent-selector and cluster rules. Without an adjacent VS15/VS16, a configured primary face keeps its glyph even when it is a deliberately monochrome emoji. Fallback discovery uses the exact Unicode 18 Emoji_Presentation property from Ghostty's pinned UCD, not a Unicode block heuristic. The separate generated range table adds 640 bytes without widening the packed width/grapheme lookup table. Only a selector immediately after the base changes its requested presentation; later selectors, keycaps, modifiers and tags cannot override it. Component discovery uses each component's own default, while whole-cluster validation requires the base's explicit presentation and permits either presentation for other components.
Color coverage uses the existing pinned HarfBuzz library's public COLR v0/v1 and bitmap glyph APIs. CoreText's sbix-face rule and macOS SVG support are retained; SVG is not enabled for the FreeType path. Glyph results are cached per resolver, and rejected candidates do not dispose fonts retained by another cache entry. Where Skia exposes a mapped font stream, a HarfBuzz blob borrows it with an owned release callback, avoiding a managed copy of large bitmap tables. This checks available color data, not an interactive guarantee for every font format/backend.
TerminalTypefaceCollection supplies ordered faces for regular, bold, italic and bold-italic logical styles. The shared renderer and glyph-coverage source use these collections, including ordinary family/file configuration. Hosts may set TerminalControl.TypefaceCollection or use the renderer/coverage CreateWithTypefaces factories to provide multiple faces per style. Collections copy entries but borrow typefaces: callers keep every face alive for the lifetime of all consuming renderers/resolvers, including retained presentation resources. Passing null to the control restores family/file configuration.
Following Ghostty's CodepointResolver, configured style faces are searched in order, then regular configured and loaded fallback faces, before regular-style discovery. Discovered faces are reused for later scalars, but must match explicit or Unicode-default presentation; configured faces accept any presentation by default. Final ANY lookup only considers loaded regular faces. Like SharedGrid, both scalar hits and misses remain cached even as the collection grows. Cache keys additionally preserve the managed API's culture argument. New configuration creates a new collection/resolver; the bounded glyph-coverage cache resets its resolver on eviction. Cluster selection, cursor, IME and password rendering use the same collection policy. Resolver-owned discovery faces are released once; configured faces are never disposed by borrowed-collection consumers.
Persisted FontFamilies settings supply four immutable ordered lists (regular, bold, italic, bold italic). The settings editor accepts one family per line and keeps comma-containing names intact. Profiles, launch configuration, runtime settings and split panes preserve the lists. Existing profiles and empty lists keep their single family/file behavior. A non-empty regular list overrides that primary selection; unavailable entries are skipped, and an entirely unavailable list restores the legacy primary. Missing/empty styled lists use variants of the loaded regular families, then regular-face fallback. Caller-supplied TypefaceCollection still takes precedence over persisted family settings.
This follows Ghostty's repeatable per-style family configuration, not the implicit DirectWrite/canvas family fallback of Windows Terminal/xterm.js. Skia family style sets distinguish unavailable families from implicit system substitution without enumerating or reordering the system fallback registry. Configured face references are owned by each renderer/coverage source and disposed after their consumers; partial loading failures dispose all acquired references. Skia protects shared system faces from public disposal and may keep their native resources cached; this does not promise immediate OS-font reclamation. Profile JSON uses generated metadata for the new immutable family-list model. The existing profile reader is retained so omitted fields in older documents keep their property-initializer defaults.
FontFamilies.CodepointMaps adds repeatable U+XXXX[-U+YYYY][,...]=family overrides, shared by both engines. Mappings preserve order, with the last matching range winning even if its family is unavailable. The resolver lazily requests the exact regular family and caches both found and missing descriptors; a missing glyph falls through to normal selection. Successfully loaded mapping faces join the regular collection in discovery order with configured-face presentation rules. No font is loaded simply because a mapping was configured.
This follows Ghostty's CodepointResolver and last-range-wins map: an available scalar override precedes style/presentation and built-in symbol drawing. Whole-grapheme selection still verifies the base's explicit presentation and every substantive component, as Ghostty's run iterator does. Text, cursor, IME and coverage share the mapping policy without changing stored terminal text. Windows Terminal's DirectWrite mapping and xterm.js's canvas/custom-glyph switch do not define this explicit range-override policy.
Mappings use the same profile, settings and split-pane propagation as family lists. Invalid editor syntax displays a line-specific error and blocks Apply and Save; the last valid mappings remain in the profile if the user switches profiles. Programmatic malformed mappings throw FormatException during normalization. Configuration accepts Ghostty's 21-bit range bounds, while non-Unicode-scalar values cannot match rendered text. Updating host configuration rebuilds the renderer and its caches immediately; unlike standalone Ghostty's new-terminal-only reload behavior, this preserves RoyalTerminal's live settings contract.
RegularStyle, BoldStyle, ItalicStyle and BoldItalicStyle select advertised face names within configured system families. Empty/default uses automatic selection; false independently disables bold, italic or bold italic. Disabling one component does not disable the combined style. Regular cannot be disabled. Names apply to explicit families, their inherited regular-family variants and the existing primary-system-family setting, not a primary font file. Missing names continue normal family fallback and missing-style completion; they are not silently replaced by a nearest weight. The Skia adapter compares advertised names case-insensitively, retaining the caller spelling in profiles. The ReactiveUI editor, profile serialization, live settings and split panes carry these controls. Disabled styles route to regular before font discovery without changing SGR attributes, colors or stored text. Borrowed collections expose the same immutable policy and retain mappings when copied. This follows Ghostty's Config.finalize family inheritance, SharedGridSet named descriptors and CodepointResolver disabled-style rule; Windows Terminal and xterm.js instead delegate logical weight/slant selection to DirectWrite and canvas respectively.
SyntheticBold, SyntheticItalic and SyntheticBoldItalic default to enabled. They follow Ghostty's missing-style completion: real configured styles win, including deliberately named faces whose intrinsic weight/slant differs from the requested category. Automatic family lookup rejects a nearest regular face when a real styled face was requested. Missing italic and bold are synthesized from the first regular face with non-color text; missing combined style first italicizes an existing real bold face, otherwise emboldens the completed italic face. Disabling a synthesis toggle aliases regular outlines for that missing style; it does not disable an available real face. The combined toggle is independent, but its resulting outlines still depend on the chosen base face. false named-style disables take precedence over these toggles.
Synthesis is carried in the resolved font, not inferred from the requested style or native face handle. Mapped glyphs and discovered/regular fallback faces do not inherit synthetic effects. Run boundaries, ASCII/general resolution caches, batched rows, shaped/Pretext/cell blobs, paths, cursor text and IME/caret drawing retain this distinction. No font bytes are cloned and no extra discovery is performed for warm synthetic lookups. Pretext's direct fallback also reuses the font cache instead of constructing a native font for each draw.
The shared Skia path uses native emboldening and a 15-degree italic shear without changing HarfBuzz glyph identity or terminal cell metrics. The macOS smoothing path uses a matching CoreText font transform plus Ghostty's size-dependent bold stroke; transformed bounds and stroke padding participate in bounded glyph-mask caches. Global rasterizer emboldening remains separate and is combined without double-applying the effect. Skia rasterization is not claimed pixel-identical to Ghostty's FreeType/CoreText backends. A caller-supplied emoji-only collection stays usable without synthetic text, rather than rejecting the whole configuration when Ghostty's text-base completion reports DefaultUnavailable; a focused test records this deliberate borrowed-collection compatibility choice. Profiles, live settings, compiled controls and split panes persist all three toggles. Regression tests pass in the automated suites; this does not imply pixel-identical platform rendering.
Ordinary printable ASCII uses lazy 95-entry tables per used style for the first lookup culture. Other cultures, explicit presentation selectors and Unicode retain the general result dictionary. Both hits and misses remain sticky; disabled styles share the regular table. This reduces common-path hashing and dictionary storage without loading fonts eagerly. Symbol override decisions also retain positive/negative scalar results rather than repeating native glyph coverage on every draw. Configuration validation no longer constructs parsed range arrays or family strings when callers only need validity. The --font-lookup benchmark compares warmed direct-table and general-cache queries with the same ASCII/style/face workload. The harness completed during closeout; paired microbenchmarks do not establish an end-to-end application speedup.
Fallback discovery now continues beyond a rejected first platform match, as Ghostty's candidate loop does when character coverage has the wrong color/text presentation. The existing Skia family/global first match and macOS emoji fast path remain first, preserving successful platform/locale choices (including CJK). Only an unresolved request opens the additional candidate stream:
- Linux uses Fontconfig's active configuration, charset/language substitution, monospace preference and untrimmed sorted results. Equal cmap coverage must not eliminate alternate presentation. Render-prepared file/collection indexes retain native ordering and sysroot handling; fonts load only as consumed.
- macOS queries codepoint-constrained CoreText descriptors, removes the temporary charset restriction, and ranks monospace, exact style, slant, weight, fuzzy style and glyph count.
head/OS/2traits and variation values refine symbolic traits. Variation axes use stable OpenType identifiers rather than localized display names. Exact PostScript names map to Skia family entries or their original font file/collection face; this avoids silently substituting another family. - Windows examines system and per-user font directories, including all TTC/OTC collection faces, alongside application-registered fonts. Inaccessible, missing or invalid files are skipped without suppressing later candidates.
- The complete Skia family/style registry is also available after native candidates, covering manager-private registrations and systems without optional native APIs.
This ports exhaustive candidate coverage, not identical font choice to standalone Ghostty: successful Skia first choices retain the shared renderer's existing host contract. Windows Terminal obtains its primary fallback from DirectWrite and xterm.js from browser/canvas. No process-global font registry is mutated here. Native query resources close before faces are transferred; early iterator disposal does not dispose a selected face. Even source-internal discarded probes use the resolver's ownership guard. Configured, mapped and already-retained faces remain protected. A reference-identity retention set replaces repeated scans of every cached scalar/descriptor during rejection. Positive and negative scalar caching, regular-only collection discovery and loaded-face reuse keep the expanded search off warm rendering paths. More complete cold misses necessarily inspect more fonts; there is no claimed overall discovery speedup.
The --font-candidates workload measures fresh mixed-presentation/unsupported queries separately from warmed hits/misses. Candidate iteration, ownership, failure cleanup, ranking/trait parsing, native descriptor smoke and collection header tests are included in automated validation. Native desktop certification and historic before/after comparisons are not implied. The separate font discovery benchmark compares single-face discovery with loaded-collection reuse; neither isolated workload proves end-to-end rendering performance.
Unsupported cells now carry an explicit TerminalFontResolution.ReplacementCodepoint: the entire cluster becomes U+FFFD, falling back to a space if that glyph is unavailable. Like Ghostty's run iterator, absence of even a space is a font configuration error. The final configured-face lookup permits any presentation after presentation-specific discovery fails. Render-only copies preserve the original width and metadata while stripping suffix VS15/VS16 after font selection; ZWJ and other shaping controls remain. Shaped text, optional Pretext, cell-anchored fallback, thickening, IME preview/caret and block-cursor text all consume this same projection, so text hashes and cluster/grid offsets agree. Stored cells, copy/search text and snapshots retain the original characters. Selector-normalization caching is bounded by both entry count and retained UTF-16 characters; warmed projection reuses its buffers. Generic LastResort range placeholders are rejected during discovery. Glyph-coverage queries do not advertise replacement glyphs as coverage, and the password indicator retains its geometric fallback. Windows Terminal's DirectWrite mapping and xterm.js's canvas delegate fallback to their platform font stacks; this shared Skia layer follows Ghostty's explicit replacement/selector policy instead.
TerminalFontWarmup.StartAsync() is a best-effort, one-shot macOS startup query. The demo starts it before Avalonia initialization, after rejecting inert toast activation. Embedders can call it at the same point in their composition root. The returned task owns its temporary font-manager resources, never touches UI or renderer objects, and reports unsupported platforms/query failure as false. Rendering does not await it; ordinary font discovery remains available if it fails or has not finished. No process-global task or renderer-owned cache is introduced. GPU warmup is separate and is not claimed here.
Windows Terminal initializes DirectWrite fallback in its renderer, while xterm.js warms ASCII atlas entries through idle callbacks. Neither defines macOS font registry behavior; this port follows Ghostty while retaining Skia's public APIs and ownership rules. Windows Terminal passes the configured family and full text to DirectWrite; xterm.js delegates presentation and fallback to its canvas font stack. RoyalTerminal instead uses the explicit Ghostty policy above, with Skia for host discovery and HarfBuzz for color-table coverage. Tests use deterministic font fixtures for family mismatch, coverage, primary ownership, negative caching and whole-grapheme fallback. --font-discovery measures fresh-resolver emoji discovery; adding --warm-font-registry reports background query time separately from subsequent foreground font initialization. These are isolated font timings, not a first-window or end-to-end rendering speedup claim.
Managed color publication
Ghostty's shared dynamic-palette defaults avoid duplicating configured defaults per terminal. Managed color state already shares its immutable configured theme; it now also caches its latest effective theme and palette independently. Repeated identical OSC overrides and resets of absent overrides reuse the render view. Foreground/background/cursor changes retain the existing materialized palette. Actual palette changes, a different configured palette and snapshot installation invalidate the appropriate caches. Each owner retains at most one effective theme/palette, not a history of colors.
Equal-to-default application overrides still acquire and retain their explicit mask, survive configuration changes and reset to the newest configured default. Snapshot nullable defaults remain protocol state rather than host render fallbacks. Published themes/palettes and COW row readers remain immutable. Palette batches copy each backing array once and apply both configured and application explicit flags directly; WithColor also avoids its former redundant defensive recopy. Public constructors and exported arrays still make defensive copies. Screen theme revision, invalidation and recoloring behavior is unchanged.
Windows Terminal RenderSettings and xterm.js ThemeService also distinguish current colors from reset defaults. Ghostty defines the managed override-mask/configuration contract; the immutable caching is a CLR-specific equivalent, not a transplant of native palette storage. Eighteen new cases cover sharing, repeat allocations, dynamic changes, equal defaults, configuration, reset, invalid batches, defensive ownership, snapshot installation and held/COW publication. These tests and --managed-colors are included in automated validation. Its six scenarios separate repeated OSC colors, cursor changes with an overridden palette, palette changes/resets and immutable palette copies. Allocation and timing comparisons with the previous implementation are not inferred from current-build measurements; no measured speedup is claimed.
Managed mode-state storage
The managed engine's 28 extended DEC mode values now occupy one 32-bit field instead of a per-processor HashSet<int>. Known print/input/render checks use constant flags directly; queries and setters use a fixed number-to-flag switch instead of a linear whitelist scan followed by hashing. Supported mode numbers are exposed internally as an immutable span. Creation, reset, updates and reads need no mode-container heap allocation or capacity growth, including the mode commit after synchronized-output staging and raw snapshot-mode installation.
This follows Ghostty's fixed mode lookup and packed mode state, and applies the fixed-heap reduction principle from Ghostty #14138. Windows Terminal's dispatch mode enumset and xterm.js's explicit mode fields were also checked. The CLR layout is deliberately private: snapshot-v1's 43-bit current/saved/default order, semantic setters and all reset/hold/mouse side effects remain unchanged. Unknown and separately stored mode numbers do not alias an extended-mode bit.
New regression cases cover every mode, independent value copies, set/reset/query cycles, the full 16-bit lookup domain, unknown/out-of-range numbers, built-in defaults, zero-allocation container operations and first snapshot installation. --managed-mode-state pairs the former hash-set/scan representation with packed storage for creation, known flags, numeric queries and toggling. --managed-print also includes mode-churn and grapheme-mode workloads for actual processor costs. Tests and CI results are recorded below; historic before/after profiling is not claimed; neither a measured throughput gain nor a whole-terminal heap reduction is claimed.
Managed tabstop storage and controls
Managed tab stops now use one bit per column, with 512 columns stored inline in the owning object and additional storage rounded to 64-bit words. There is no separate array for ordinary widths. Forward/backward navigation skips empty words with bit scans; repeated forward tabulation stops at the right margin. Styled VT export enumerates stops in order without allocating and sorting an integer array. Binary export uses bounded 64-byte stack scratch instead of a new bitmap; restore imports the packed little-endian words directly.
This ports the compact-storage principle of Ghostty #13600 and preserves idempotent clear behavior from Ghostty #13900. The comparison also closes missing CTC commands (CSI W, CSI 0/2/5 W) and DECST8C (CSI ? 5 W). Defaults now match pinned Ghostty's bitmap exactly: every eighth column, excluding zero-based column zero and the final column. Explicit stops at either endpoint remain legal. HT, CHT, CBT and tab-clear/set commands preserve pending wrap, as in Ghostty's terminal/stream handler.
Windows Terminal's tab dispatch uses column storage and xterm.js's buffer uses a sparse object with scalar navigation. Their edge-clamping behavior was compared; RoyalTerminal follows Ghostty for default endpoint bits and strict CTC/DECST8C parameters rather than combining differing behaviors. Width changes stage a replacement bitmap and reset defaults only on successful resize; height-only resize retains custom stops. Existing snapshot bytes, including explicit endpoint stops, remain accepted.
Focused cases cover word/inline/tail boundaries, exact packed round trips, bounded navigation against a scalar reference, malformed controls, pending wrap, margins, both snapshot formats, resize rollback and native differentials. --managed-tabstops compares hash-set and packed creation, sparse forward/backward navigation, ordered export and bitmap export at 80/512/521/4096 columns. The expected gains are smaller storage, fewer lookup operations and removal of temporary export arrays. Tests, benchmark execution and CI are recorded below; current measurements alone do not establish a historic before/after speedup.
Unknown APC policy and progress lifecycle
Both VT adapters implement ITerminalUnknownSequencePolicy. Its UnknownSequenceMaxBytes property defaults to 4096 payload bytes, accepts zero to disable capture, rejects negatives and survives resets. Managed construction also accepts BasicVtProcessorOptions.UnknownSequenceMaxBytes. This connects the previously native-only configurable capture limit from Ghostty #13702.
The managed APC identifier state now matches the pinned native parser: empty APCs and unfinished 25a1 glyph prefixes produce no unknown callback. Complete recognized Kitty/glyph commands retain their independent limits and are not reclassified as unknown merely because their protocol is disabled or invalid. Unknown capture freezes its limit when it starts; policy changes can affect a still-unclassified identifier, but not an active capture or an ignored command. Small limits apply to the identifying prefix too, preserve the truncation flag and never reinterpret discarded payload as a new Kitty command. Captured arrays are callback-owned; CAN/SUB-aborted unknown commands remain silent. This policy is host configuration, not snapshot state.
Unknown capture also follows pinned apc.zig's allocation-failure contract: failed growth drops that append, preserves earlier bytes and marks the report truncated; later appends may still succeed. A failed final byte-array copy reports empty, truncated content. Identification preserves native prefix, semicolon/overflow suffix and bulk-payload append boundaries. The four-byte identifier is inline, separate from recognized glyph body storage. Capture scratch grows only on demand up to the command's frozen quota. Managed scratch up to 4096 bytes is reused; larger buffers are released at command boundaries, or transferred directly to the callback when exactly filled. Lowering the inactive quota trims retained scratch. This bounds idle retention, but can increase allocation for repeated, large fragmented captures compared with a permanently retained list. Callback/record allocations and arbitrary consumer exceptions are not covered by the byte-storage allocation-failure guarantee.
The capture reference remains Ghostty #13702. xterm.js's APC parser uses registered handlers or incremental fallback and rejects oversized handler payloads; Windows Terminal's dispatch does not define this bounded truncated raw-capture API. Neither replaces Ghostty's allocation-failure semantics here. Thirty-four focused cases cover failure injection, append boundaries, owned results, scratch retention, cancellation, identifier storage and glyph-body delivery. --managed-unknown-apc pairs the former reusable-list capture with the bounded owner for six small/full/clipped/fragmented/large workloads. Expected benefits are no identifier heap storage, no small scratch regrowth and no idle large unknown buffer. Measurements, test execution and CI are recorded below; no end-to-end throughput improvement is inferred.
Managed OSC 9;4 progress reports are host effects, not snapshot state. Every protocol RIS sends a remove report, including repeated resets with no preceding progress report, following the pinned stream-handler implementation and Ghostty #13901. Explicit remove commands still deliver their reports. Soft reset, screen changes and snapshots do not invent progress effects. Programmatic terminal reset emits no remove effect, matching native ghostty_terminal_reset, which resets the terminal rather than dispatching protocol RIS. A newly restored snapshot does not replay host progress state, but a subsequent RIS still sends remove.
Windows Terminal's dispatch was checked for unknown-sequence and ConEmu progress routing; xterm.js provides embedder APC/OSC registration. Neither defines Ghostty's bounded raw capture contract, so the pinned Ghostty parser/effect lifecycle is the reference. Fifty-six new cases cover both adapters, identifier boundaries, mid-command policy changes, quotas, array ownership, reset persistence and progress effects; exact callback sequences are checked against the native processor, including unconditional repeated RIS removal. Platform/CI validation remains tracked in the pull request's validation table.
Managed DCS reply formatting
DECRQSS requests are decoded from their bounded two-byte parser payload without copying a DCS array or creating strings. SGR, margins, cursor style and unsupported replies use 256-byte stack formatting; the callback receives one exact-size owned array. SGR keeps native ordering, compact ANSI/palette identity and colon RGB syntax, including overline and underline variants but not underline color. This is the reply-path application of Ghostty #13587's direct-formatting principle, following the pinned dcs.Command.DECRQSS and Terminal.printAttributes contract. Styled VT replay continues to use its separate, richer SGR formatter.
XTGETTCAP now normalizes bounded hex keys on the stack and looks up the frozen table through a character span. The generator emits the actual longest key bound alongside all 272 capabilities. Replies write ASCII directly into their owned array, preserving uppercase key/value spelling, boolean capabilities with no equals sign, ignored unsupported keys and the configured per-processor TN name. TN validates its 128-byte UTF-8 bound before allocating a response and encodes the name through stack storage, without temporary UTF-8/hex strings. Small query batches use 512 bytes of stack storage; larger batches reuse pooled storage within the existing 1 MiB parser limit. Storage is isolated from live parser buffers before any callback and returned/cleared in finally; callback arrays never alias stack, pooled or shared data. Queries without a reply consumer skip encoding.
Reference decision: pinned Ghostty's supported query subset and reply spelling remain authoritative. Windows Terminal also reports real SGR state but supports additional status requests/underline color, whereas xterm.js reports a fixed reset-only SGR response and exposes XTGETTCAP to embedders rather than a built-in table. Those differences are not imported into native parity.
New tests cover every palette index, all underline styles, exact streamed/native status replies, owned callback arrays, no-consumer/oversized requests, UTF-8 TN limits, mixed/invalid/unknown capability keys, and stack/pool-sized batches. --managed-dcs-replies pairs the former string/list pipeline with bounded formatting for default/palette/RGB SGR and color/boolean/TN capability replies. The expected reduction is intermediate allocation and transcoding, not elimination of the callback's owned-array cost. Tests, benchmark measurements and CI results are recorded below; no end-to-end throughput gain is claimed.
Managed search capture invalidation
The search-only COW capture now has a lazy, buffer-specific change token. An unchanged capture checks token identity/revision, columns, viewport height and screen selection in constant time without allocating or walking history. The first search capture attaches row observers; terminals that have never searched do not allocate a token. Writes through row spans/indexers, width/wrap changes, cell-storage swaps and structural row-buffer changes invalidate it independently of renderer dirty acknowledgments. Read-only access, unchanged layout setters and semantic-only row metadata do not invalidate search.
For active-only edits since the latest successful capture, row observation also proves that the history prefix is unchanged. Capture skips its whole 64-row reference blocks and compares only the active rows plus at most 63 boundary rows, retaining the weak storage cache. Changed captures still clone the outer block-reference array when needed: this is O(history blocks + active rows), not constant-time capture. Direct history edits, structural changes, lost observers and stale consumers fall back to exact O(total rows) storage/layout comparison. Conservative invalidations may publish a new capture with the same row blocks. A stamp never changes after publication, so one search consumer cannot advance another consumer's view; the dirty scope is acknowledged only after allocation of the complete new capture succeeds.
Tokens contain no owner references and are not copied into COW row wrappers. Snapshots therefore do not retain live screens/history through their observer. Transferring observation of an aliased row invalidates its previous token; a full capture reattaches even rows in unchanged blocks. Retired rows can conservatively invalidate a former owner without retaining it. Revision saturation permanently disables the shortcut rather than risking stale equality after rollover. The existing screen-lock/no-retained-writable-cell-reference contract is unchanged.
Active/history membership uses a spare row-metadata bit, not another per-row object or index. A wrapper present in both regions is always treated as history. Structural edits or a different active-window boundary replace the token and rebind membership, so recycled history rows become active without retaining a permanent full-scan penalty. This rebuild is lazy and happens once at the next capture, not per added/removed row. Viewport scrolling does not change which terminal rows are mutable. Wrapped and hard-break matches across the retained history boundary continue through the existing KMP checkpoints.
Ghostty search retains history work, gates active search on content dirtiness and tracks screen generations. Windows Terminal's mutation ID and xterm.js's line-cache invalidation also avoid treating renderer state as search validity. The managed token is a CLR-specific invalidation mechanism, not a transplant of Ghostty's page allocator or xterm.js's event/timeout cache.
New cases cover row/structure mutations, aliased wrappers, independent COW owners/consumers, adoption, both sides of storage swaps, renderer acknowledgment, screen/viewport transitions, slices, saturation and weak ownership. Active-window cases add direct-history fallbacks, boundary blocks, both-region aliases, recycled membership, stale-consumer capture and cross-boundary search equivalence. These cases and --managed-search-capture are included in automated validation. The benchmark separates idle, first/last active-row edits, history edits, wrap changes and scrolling at 1,024/16,384 rows, plus a paired observed/unobserved write loop to quantify notification overhead. Current-build allocation and timing results are recorded below; these are not a historical before/after speedup claim.
Managed snapshot grid transport
The grid codec ports the row-transport work from Ghostty #13848: vectorized encoding, narrow decoding, per-row wide repair and batched grapheme encoding. Runtime-vectorized trailing-zero detection and OR classification select the same 1/2/4/8-byte row width. Vector<T> narrowing packs 1/2/4-byte output, while 8-byte output bulk-copies the managed wire words, which already contain link IDs. One/two-byte input widens in vectors; surrogate BMP lanes become U+FFFD before widening. Full-word semantic normalization remains scalar. Only rows containing width markers run the separate wide-pair repair, including elided default cells beyond an encoded prefix.
No hardware-vector or little-endian assumption leaks into the wire contract: scalar tails and nonaccelerated/big-endian fallbacks retain explicit little-endian conversion. Helpers validate spans before writes and operate within the existing 4 KiB stack buffer. Grapheme entries share that buffer across small records; larger entries flush then stream in bounded chunks. Suffix codepoints bulk-copy on little-endian hosts, entry order remains row-major and empty suffix maps avoid a grid scan. An oversized owned suffix is rejected instead of truncating its 16-bit length field. Decode limits, suffix filtering, page ID resolution and live allocator reconstruction remain unchanged.
Suffix decoding additionally counts valid nonzero scalars with runtime vectors before allocating the exact admitted output size. All-valid little-endian entries bulk-copy into owned arrays; malformed entries retain scalar filtering in wire order, including a scalar tail/nonaccelerated/big-endian path. NUL, surrogates and values above U+10FFFF do not consume the raw quota. Validated raw arrays also supply the live restore's prefix count without decoding those same bytes again. Its independent 64-suffix cap, allocation replacement boundaries, whole-prefix failure and duplicate retry order remain intact; raw retention can still be longer than live storage. This extends the buffered suffix work in Ghostty's codec change without changing the lossless/raw versus bounded/live contract. New scalar-oracle, quota, ownership, duplicate/framing and allocator-replay cases are included in automated validation; the grid benchmark adds long-valid and filtered suffix workloads.
Windows Terminal text export and xterm.js serialization do not define this binary format; Ghostty remains the wire reference. New scalar oracle, all-BMP, lane/tail/alignment, sentinel/bounds, width classification, suffix batching and zero-allocation cases are included in automated validation. The --managed-snapshot-grid harness separates encode/decode for blank, ASCII, BMP, styled, linked, wide and dense/sparse-grapheme grids. Before/after measurements, hardware-disabled execution, native golden/round-trip tests and full-suite results are recorded below; automated checks are not interactive platform sign-off.
Managed snapshot software checksum
The managed framing checksum now ports Ghostty's slicing-by-16 CRC32C and three-stream interleaving. SSE4.2 and ARM CRC hosts retain the existing BitOperations.Crc32C word loop. Without those instructions, spans use 16-byte slicing, with three independent CRC chains for inputs of at least 4096 bytes. GF(2) zero shifts combine the chains without changing initial/final XOR, little-endian framing or streaming boundaries. Private, immutable-after-initialization polynomial tables use 16 KiB; the separately initialized even-power matrices use 2 KiB, sufficient for every int-sized span. Hardware-only use does not need these software tables. Warm updates require no per-call allocations.
The expected benefit is reduced dependency-chain latency on non-CRC hosts; hardware throughput is intended to stay unchanged. No measured gain is claimed. The --managed-snapshot-checksum harness pairs the previous runtime word loop, forced software and automatic selection over eleven payload sizes. Run it both normally and with DOTNET_EnableHWIntrinsic=0 in the final validation phase. New bitwise-oracle, high-power matrix, alignment/tail, 4096-byte threshold, streaming, golden-record, corruption, concurrency and warm-allocation tests are included in automated validation. Full-suite and native round-trip results are recorded below; interactive platform sign-off is separate. Windows Terminal text export and xterm.js SerializeAddon, linked above, have no equivalent binary checksum format; Ghostty remains the wire reference.
Managed snapshot record scratch
The managed binary snapshot encoder follows Ghostty's bounded record-scratch change: one reusable record buffer starts with 512 bytes on the stack and rents a larger buffer only when required. Larger records reuse that rental; retired buffers are returned with clearing on growth, success or failure. C# ref-struct lifetime rules and constrained generic codec sinks prevent boxing or escaping stack pointers. The existing Stream codec entry points use an allocation-free value-type adapter.
Record and cumulative payload bounds are checked before scratch writes/growth. The buffer does not change record framing, CRC32C, page order, continuation, snapshot ownership or caller stream lifetime. Source-page capture still owns managed objects; this is not an allocation-free whole-snapshot claim. The --managed-snapshot-encode benchmark measures full encode to Stream.Null, with terminal setup and returned byte-array ownership excluded, to compare allocation and throughput before/after this change.
Windows Terminal's text/VT export and xterm.js's SerializeAddon were checked; they do not define this binary record format. Ghostty remains the wire-format reference. Golden codec, native round-trip, stack/pool boundary, failure cleanup, zero-allocation small-record and exact-limit tests cover the port.
Host visibility reports
Both VT processors implement ITerminalVisibilityState, following Ghostty #13494: CSI ? 998 n queries and every enable of mode 2033 report current host visibility; effective changes report only while enabled. CSI ? 999 ; 1 n means potentially visible, and CSI ? 999 ; 2 n means known hidden. Repeated host assignments are silent. Visibility survives RIS, API/session reset and synchronized-output publication; it is not serialized into snapshots or inferred from keyboard focus.
TerminalControl observes itself and its visual ancestors, opacity, window hide/minimize and attachment. The visibility state is installed before a new VT processor consumes input. Updates use the same screen synchronization and response path as parsing, so they do not corrupt incomplete CSI/OSC input or wait for a render frame. External endpoints can opt into ITerminalVisibilitySink; old endpoints and ancestors stop receiving updates when detached. Unknown compositor occlusion/workspace suspension remains potentially visible: Avalonia's public host state does not expose Ghostty GTK's compositor suspension signal.
The native embedding adds two repository-owned visibility accessors. The setter uses Ghostty's encoder and a caller-owned nine-byte buffer; it does not replay VT, allocate, retain pointers or call through a private wrapper layout. The managed wrapper returns owned response bytes for ordered delivery, and the native VT adapter forwards these to its normal response callback.
Windows Terminal and xterm.js were checked for their separate focus-reporting paths. Their focus mode is not used as a substitute for Ghostty visibility reports. Focused protocol, native argument/buffer, snapshot and Avalonia lifecycle tests cover this distinction.
Hidden-terminal rendering resources
Both VT engines use the same visibility-aware Skia presenter. Hiding the presenter or an ancestor, setting an ancestor's opacity to zero, minimizing its window, or detaching it releases the owned retained framebuffer and compiled framebuffer shaders. Hidden output and cursor invalidations do not schedule composition frames. The terminal model continues to receive output; showing it recreates resources lazily and fully redraws the latest screen, size, and shader configuration, including rows whose dirty flags were already acknowledged.
This follows Ghostty's hidden-surface resource release, xterm.js's hidden refresh suspension, and Windows Terminal's full viewport refresh when painting resumes. It is not a transplant of Ghostty's Metal/OpenGL swap chain.
Avalonia composition messages do not guarantee a current GPU context. The framebuffer owner retains the borrowed context identity from Avalonia's public Skia platform lease, re-enters it for release, and never disposes that context. Retained GPU surfaces are unbudgeted so releasing a hidden framebuffer does not leave it in Skia's shared resource cache. Context loss is handled without issuing GPU operations; a custom backend without a re-enterable platform context uses a raster framebuffer. Shared font/image caches and other terminals' GPU resources are not purged. OS occlusion is not inferred from focus, and a visible background terminal keeps rendering.
Headless coverage exercises hidden startup, ancestor/window/opacity transitions, latest-state redraw, shader configuration ownership, reparenting and late messages. A 20-hidden-plus-one-visible 960×600 fixture checks that hidden retained framebuffer ownership falls from 46,080,000 bytes to zero while the visible framebuffer remains allocated; this is not a whole-process or GPU-memory claim. Native macOS CGL tests exercise release outside a current context, shared-target survival, context loss/abandonment/disposal, retry, and raster fallback. The presenter synchronizes its composition size after layout assigns new bounds; antialias clip outsets are not treated as DPI scaling or framebuffer dimensions.
Batched managed printing
Managed input now writes eligible narrow and wide runs with one row-local cell template and one writable COW span. ASCII uses the runtime's vectorized range scan without decoding; UTF-8 and REP use bounded 256-codepoint stack storage. Snapshot-backed rows update contiguous old-style reference counts in groups. Wrapping, page transitions, complex cells, hyperlinks, grapheme joins, character sets, insert mode and disabled wrap retain the scalar printer's semantics. Incomplete UTF-8 remains with the streaming decoder. Managed cells contain CLR references, so Ghostty's packed-u64 SIMD stores are not copied into their layout.
The behavior follows Ghostty's printSlice implementation and batched REP change. Windows Terminal's string writer and xterm.js's print/REP handlers also share printing paths. RoyalTerminal retains Ghostty's last-codepoint REP, not xterm.js's whole-grapheme extension.
ManagedPrintSliceTests compares bulk input against byte-at-a-time scalar input, native Ghostty, retained COW readers, snapshot style capacities and round trips. Three seeded edit/resize sequences add 900 mutation checkpoints. The maintained --managed-print benchmark compares printing only, without PTY or renderer work. One local macOS arm64 Release comparison against b649ce2, with tiered compilation disabled in both processes to exclude tier-promotion timing, measured the median of seven 25,000-feed samples after 25,000 warmups:
| Workload | Before (ms) | Batched (ms) |
|---|---|---|
| ASCII row redraw | 71.151 | 7.823 |
| Narrow Unicode row redraw | 92.116 | 16.312 |
| Wide-cell overwrite (scalar cleanup) | 79.386 | 78.839 |
| REP | 72.421 | 11.105 |
| Alternating styled redraw | 142.780 | 18.101 |
| Snapshot-backed styled redraw | 1244.675 | 267.859 |
| DEC special charset (scalar) | 71.262 | 75.185 |
Allocations were unchanged: zero in the untracked workloads and 100,000,000 bytes per sample for snapshot-backed styled redraws, including their existing SGR/page bookkeeping. These isolated numbers do not establish application throughput, cross-platform performance, or gains for every fallback workload.
Snapshot history accounting
Incremental history admission measures logical Ghostty page bytes, not CLR heap usage. Restored PAGE capacities are retained, including after partial pruning. New live rows are assigned standard pages using their physical width and native metadata defaults; subsequent admission checks reuse unoccupied tail slots rather than charging another page. Slot identities survive COW and row rotations. Recycling a CLR row after scrollback eviction releases its historical page identity.
Capacity growth observed at an admission checkpoint follows native default, doubling and saturation rules. Styles and graphemes can project the current usage over reserved rows with 25% headroom, bounded to 32 times the previous request and the native four-GiB page ceiling. Admission uses usable set/map slots and rounded bitmap chunks rather than treating raw capacity hints as usable item counts. Grapheme replacement scratch is charged once, not once per retained cluster.
Measured growth is retained through later erases. An immutable replacement identity updates only the live row set, so synchronized-output and search copies retain their own charges. A page's charge disappears only when its last row leaves that view. These rules follow Ghostty PageList.grow, increaseCapacity and page replacement ownership; Windows Terminal and xterm.js use row-oriented storage and do not define this snapshot budget. The managed hard scrollback row cap remains independent of the logical native-page budget.
Changing TerminalScreen.SnapshotScrollbackQuota immediately evicts eligible whole historical allocations in both buffers, using native byte/line minimums. A page overlapping the active area is never split just to meet a quota. Row growth enforces the line limit; a new tail allocation can recycle one old page for byte pressure, but reuse of a free tail row does not re-enforce bytes. Resize enforces lines after layout, before combined anchor remapping. Zero bytes also disables scrolling into retained boundary history. These are PageList limits: unlike Ghostty's terminal-level zero-byte setter, the snapshot policy intentionally preserves resident READY overlap instead of erasing partial primary history. Removed anchors are invalidated; surviving anchors and raster placements shift once, and COW readers retain their original rows. Host policy changes reach held live input and publication without reapplying an unchanged byte setter on every batch. Host row rotation can interleave allocation identities: their connected prefix remains indivisible, and a byte-growth recycle cannot remove multiple allocations solely to work around that host representation. These conservative cases can remain over quota. Runtime-limit, streaming, resize, ownership and native continuation regression cases are included in automated validation; isolated measurements do not establish a whole-application speedup.
Column reflow now carries source-page allocation provenance through logical lines, including lines that cross PAGE boundaries. The first destination inherits the first source's adjusted capacity; later destinations inherit the page currently being consumed. Deferred blank rows use the next nonblank source when allocating another page. Width adjustments retain the layout's byte budget rather than using extra pooled bytes; the native first/later-page fallbacks are distinct. All-blank reflow retains the first page, and viewport padding reuses its remaining slots. Accounting is isolated from text, style and tracked-anchor movement; ordinary untracked screens do not create snapshot allocation metadata during reflow.
Reflow metadata now uses a dedicated copy path matching Ghostty's ReflowCursor.writeCell and hyperlinkStringsFit. Graphemes check map capacity before probing and freeing their suffix allocation; growth retries until the suffix fits. Hyperlinks probe URI and explicit ID as separate rounded allocations, free that scratch, then duplicate before set deduplication—even when the destination already contains the same link. Set failure releases both duplicated strings before growth/rehash, and replacement pages receive a fresh string-capacity check. Ordinary row cloning keeps its different lookup-before-allocation and failed-clone string-retention behavior.
The managed reflow copy shares immutable encoded link bytes and their cached hash, avoiding an extra URI-sized CLR copy or hash while preserving logical native allocation transitions. This is an expected hot-path cost reduction, not a measured throughput claim. Windows Terminal's TextBuffer::Reflow and xterm.js's Buffer._reflow are visible-layout references but do not define Ghostty's page-local allocator. Sixteen new cases cover map-versus-bitmap pressure, separate string rounding, duplicate scratch, growth/rehash cleanup, retained owners and repeated native resize comparisons. --managed-snapshot-reflow adds four snapshot-tracked workloads alongside the existing untracked reflow harness. Test execution and current-build measurements are recorded below; historical speedup is not inferred.
Reflow now handles the native page-growth ceiling without marking a representable destination as overflow. A later-row refusal clones the current row into a new source-capacity-derived page before removing its old ownership; the upper page retains its dead-ID/string history. A first-row refusal preserves the accepted grapheme/link/style prefix, drops only the rejected stage and subsequent metadata, and continues with following cells. A second set-insertion refusal after one growth/rehash drops only that set's field, matching native unsafe-build recovery; hyperlink failure does not prevent independent style copying. Screen payloads are copied before this filtering, including bulk runs and wide tails, so rejected metadata cannot be restored by a subsequent raw copy.
Managed split recovery deliberately resumes at the failed metadata stage instead of replaying already-cloned ownership. This retains one owner for accepted graphemes and links; it does not emulate native partial-cell replay's potential putNoClobber failure or orphan-reference history. An unexpected row/page clone failure throws into the processor's existing resize rollback instead of publishing partial allocator state. Real CLR allocation failure also propagates. Twenty-eight additional authored cases cover 4/16-KiB growth ceilings, five metadata pressure types, retained sources, split-prefix ownership, grouped/scalar/wide payload filtering, row-clone rollback with anchors, and bounded adversarial set retries. Sparse logical capacities avoid allocating four-GiB pages in these cases. Automated execution is recorded below; no four-GiB native allocation or exhaustive working-set equivalence is claimed.
Managed style copy now looks up each occupied source chunk once, scans equal-ID runs with bounded span operations and fills destination chunks in bulk. Grouped reference increments retain preferred-ID insertion order and exact accepted prefixes on failure. Page rebuild uses the same run-copy path, including logical row remaps and inline background observations. Sorting scratch for physical rebuilds uses at most 128 stack integers, otherwise a returned pooled integer buffer sized to occupied chunks, never the PAGE dimensions. This extends Ghostty's reflow style-run optimization to managed page rebuilds; it does not change the wire layout or native capacity/probe rules. Sixteen additional scalar-oracle, boundary, sparse/pool, remap, reference-release and failure cases are authored. --managed-snapshot-style-rebuild adds uniform, alternating, rotated-inline and sparse workloads for the final before/after measurements. No test execution, allocation measurement or throughput improvement is claimed.
Non-reflow widening also rejects a zero-progress row clone instead of retaining its payload with an overflow marker. An already-valid source table may become unclonable in row order when 32 colliding styles precede another style that was originally inserted first. The processor's staged transaction keeps the original rows, metadata, anchors and held output usable on failure. Partial successful pages may still be split and retried; only failure on an empty fresh page aborts. The native source overlay terminates that same formerly unbounded retry, releases the unlinked page and restores this source chunk's pins before rollback cleanup. This does not claim whole-terminal native rollback after earlier source chunks have committed. Twelve managed rollback/recovery cases and two native collision/ pin regressions are included in automated validation. Rebuild the native extension before executing the native regressions: the previous library loops on their fixture.
Quota capacity measurement now obtains styles, grapheme cells/bytes and hyperlink owners/cells/string bytes through one current-page census. It checks every supplied row revision once, rejects foreign page identities, and uses indexed access rather than boxed IReadOnlyList enumerators. Individual diagnostic queries share that allocation-free gate. Cursor-only references, occupied allocator slices and COW owners remain authoritative; dirty/untracked pages retain the existing cell-based fallback. This reduces revision walks and weak-table lookups without changing Ghostty's native allocation accounting or growth projection. Twelve new counter, single-pass, invalidation, identity, COW, host-write and warm-allocation cases are included in automated validation. --managed-snapshot-usage compares one combined query against three indexed queries for current and dirty-tail pages at four row counts; that paired baseline isolates traversal work, not the old enumerator allocations. Full checkpoint costs and before/after profiling remain unmeasured.
This models observable page pressure and supported failure policies, not exact CLR/Zig allocator identity or proof over all allocation failures. Cursor-style pressure splitting uses exact live row-layout selection, keeps the upper allocator and clones the suffix before publication. Migrating links precede the style retry; failed SGR retries its previous pen while cursor restoration/movement falls back to default. Other unconnected paths cannot reconstruct allocator history from final cells. Boundary, COW, recycling, source-provenance, blank/wrapped reflow, growth-ceiling and native admission comparison tests pass in the automated suites. These cases do not prove arbitrary allocator-failure parity.
Live snapshot restore shares Ghostty's 64-suffix-codepoint bound with terminal input, excluding the base scalar. Valid scalars beyond that bound are ignored only when materializing live cells; the raw PAGE codec remains lossless and still consumes the complete suffix and enforces caller decode limits. Unlike xterm.js's combined-string appends and Windows Terminal's row/text storage, the managed engine uses Ghostty's bound so snapshot restore cannot create a larger live cluster than terminal input. Bounded UTF-16 scratch covers even supplementary base and suffix scalars. Wire-preservation, limit and native continuation tests are included in automated validation.
Grapheme restore also models native allocation pressure while consuming entries in their original wire order. The native map limit and 16-byte bitmap chunks apply; small allocations stay within a bitmap word and replacement keeps its old slice alive until the new one fits. Failure drops the complete suffix and releases its prefix, permitting a later duplicate to succeed. The raw grid retains its existing first-valid-entry content and canonical re-encoding behavior independently of these live outcomes. The temporary bitmap materializes words only for actual bounded content, never in proportion to an untrusted capacity hint, and is released after parsing. Invalid scalars/targets do not consume logical storage. Whole-prefix failure, duplicate recovery, wire-order, huge-hint and seeded native comparison tests are included in automated validation.
Style and hyperlink restore now honor native table/string pressure too. Logical reference-counted sets reproduce Robin Hood probing, the 31-probe insertion guard, duplicate-value references, dead-ID reuse and trailing-ID reclamation. Style hashes use the native packed representation and integer mixer; hyperlink hashes include native ID tags and 64-bit slice lengths with streaming Wyhash. Set/table capacity hints do not allocate dense CLR tables.
Hyperlink IDs and URIs consume 32-byte bitmap chunks before value deduplication. Ignored zero/duplicate wire IDs still perform native insertion/release, so their dead entries may hold strings until a later insertion reclaims them. Failed URI allocation frees its explicit ID, and the fixed hyperlink-cell map admits cells in row order. Raw accepted tables remain independent of the live result. Temporary hyperlink sets/bitmaps are discarded after parsing; accepted IDs and suffixes remain. Style storage now additionally retains a decode-time allocator seed: each accepted wire entry surrenders its temporary reference after cells take their own, including equal values under different wire IDs. Dead slots and probe placement survive until reuse or rehash. Allocation identities expose isolated copies of that seed so mutable bookkeeping cannot change raw pages or held COW frames. Sparse 256-cell chunks use 16-bit IDs, avoiding per-cell dictionary entries and allocation proportional to absent prefixes or capacity hints.
The style-storage lifecycle model includes cursor-only release/add, cell write/erase, native rehash-versus-growth failure reasons, preferred-ID insertion and row-major rebuilds. A rebuild drops dead entries and leaves cursor restoration to its owner. Unit and native cursor/write/erase comparisons are included in automated validation. The processor now uses the model for each changed SGR parameter/group, including intermediate styles reset later in the same CSI, and for primary/alternate SCREEN cursor restoration. New styles are charged before printing and retain their capacity after erasure. Live entries reconcile changed row revisions before a pen change; unchanged rows are not rescanned cell by cell. Decoded inline background overrides retain their native style identity during reconciliation. Ordinary non-snapshot/non-quota terminals do not create the tracker.
Live allocator entries use weak page keys and fork on COW mutation. Style-only changes do not copy terminal cell arrays; synchronized-output publication transfers the private allocator state with its row ownership. New empty/collision pressure, rehash, transient-write, cursor-restore and COW/publication comparisons are included in automated validation. The immutable seed remains decode/rebuild-time state, separate from the tracker owned by the mutable screen.
Cursor movement now transfers the style reference on page changes, including explicit positioning, index/reverse index, text wrapping, Kitty placement's line feeds and the optional Sixel cursor advance. Same-page movement retains the reference. DECRC installs the saved pen on the departure page before movement; 47/1047 screen switches and 1049 entry install the incoming pen at the dormant destination cursor before copying its position. A 1049 return instead resumes the dormant primary cursor before restoring, without copying the alternate pen. Compound DEC mode parameters observe each intermediate movement. These choices follow Ghostty Screen.cursorChangePin, Screen.cursorCopy and Terminal.restoreCursor/switchScreenMode; Windows Terminal and xterm.js have different storage models and no equivalent PAGE allocation contract.
The pinned Ghostty resize implementation has one intentionally unported allocator quirk: partial alternate-history erasure changes a node's row-layout serial without replacing its allocation, so Screen.resize skips releasing a temporary hyperlink reference when the native page layout permits reusing that allocation. A subsequent resize then grows the native hyperlink table from 192 to 384 bytes unnecessarily; layouts that replace the allocation do not retain the orphan. Managed leases use allocation identity and release the reference; the differential regression checks identical cursor/link state, exactly one managed live link and the specific native capacity divergence in both active-buffer orders. This is not exact allocation-history parity and does not change the native dependency.
When scrollback pulling is enabled, managed VT reflow also preserves Ghostty's active-row space below the cursor, discounting new wrap continuations above it. Narrowing applies the new height before reflow; widening applies it afterward. Height growth pads below a non-bottom cursor instead of pulling history into view. This follows PageList.resizeCols's preserved_cursor policy, not Windows Terminal's fixed-buffer cursor mapping or xterm.js's ybase/cursor-line policy. Generic screen-only resizing and explicit ConPTY viewport preservation are unchanged. Differential coverage exercises both pull-scrollback settings, live and restored screens, wrapped cursors, mixed width/height changes, writes and resize round trips.
Screen-switch cursor copies now have their own failure boundary. If installing the entering style at the dormant destination fails, the destination keeps its position, pending wrap, pen, protection, cursor shape, semantic state and implicit hyperlink counter. The old pen is reacquired in the surviving allocator rather than restoring a stale page-local ID; a second allocation failure safely falls back to default. Any completed page split, buffer switch, required clear, charset transfer and hyperlink closure remains committed. Failure only when subsequently moving to the copied position still uses the normal movement fallback, not cursor rollback. Mode 1049 clears reset the dormant pending-wrap flag before a failed copy; a successful copy retains the entering cursor's wrap state. Erase operations own their wrap reset, including mode-switch clears, while history-only ED3 and ignored ED/EL parameters leave it unchanged. Cursor-copy, COW, split, output-hold and native wrap-continuation tests are included in automated validation.
Streaming tail line feeds assign slots using an owner-local high-water mark instead of measuring the full history for every new row. Checkpoint-assigned slots update that mark, and COW forks preserve independent ownership. Page transitions still reconcile row groups; this is not an end-to-end performance claim. Discarding alternate storage releases its retained cursor-page entry; clearing all storage drops the tracker. Movement, screen-switch, tail-slot, Sixel, host-clear and native capacity comparisons are included in automated validation.
Live row edits now update style references before their cell changes. Printing, wide-cell cleanup, ED/EL/ECH, protected erase runs, hidden-column removal, prompt redraw and full/partial row shifts use explicit write/clear/move operations. An erased background-only cell releases its native style even when its visible attributes do not change. Cross-page row copies clear the entire destination run first, insert using preferred source IDs and retry the whole run after capacity growth. Within a page, whole-row shifts exchange cell-array ownership and COW flags without copying arrays; partial-width shifts clear the destination and move the source run. ICH/DCH permute style references before releasing the vacated run. These distinctions follow Ghostty Page.clonePartialRowFrom/moveCells, Screen.clearCells and the terminal insert/delete implementations.
Clear operations group equal style IDs into one reference-count update, and a single reusable empty 256-cell chunk avoids repeated allocations when replacing the last styled cell. Untracked screens do not allocate the style tracker. Added tests cover identical-background erasure, protected holes, transient copy growth, preferred IDs, same-page moves, COW storage, chunk reuse and native capacities; automated execution and isolated measurements are recorded in the validation results below.
Snapshot-aware reflow, row retirement and style/grapheme/hyperlink mutation hooks are connected. The supported mutation-time contract covers observable state, quotas and deterministic failure boundaries, not exhaustive CLR/Zig allocation failure equivalence. Live grapheme append now keeps the previous suffix if growth or its single retry fails, while retaining earlier width, tail and cursor edits. Cross-page widening copies retain the source and only the accepted scalar prefix on failure, stopping before the tail and final scalar; completed same-page moves remain committed if the final append fails. Copied prefixes preserve a remapped inline base and supplementary scalars. A refused hyperlink cell-map insertion omits that cell's link but leaves a surviving OSC 8 cursor active. Failed cursor starts/migrations publish the dropped link without consuming an implicit ID. A newly registered pending protocol identity is now also removed from both managed lookup indices on refusal or thrown preparation, including implicit page migration and resize restarts. Existing/public registrations and cell-owned source identities are never discarded by that rollback. Collision chains remain immutable for COW readers; head removal needs no new allocation, and only a colliding prefix is rebuilt before publication. The token watermark rewinds only if no later registration exists. Registry insertion prepares both indices before publishing either. This follows Screen.startHyperlinkOnce's pending-value cleanup, preventing repeated refused OSC 8 values from accumulating URI/identity objects for the screen's lifetime. It does not introduce general pruning of successful public registrations. New refusal/retry, migration, resize, held-output, exception, collision/COW and weak-lifetime cases are included in automated validation; isolated registry measurements are recorded below. Windows Terminal prunes unreachable hyperlink IDs as rows retire, while xterm.js uses line-marker lifetimes. Neither defines the native PAGE refusal contract; Ghostty defines that boundary here, with stable public registrations preserved. None of these refusals marks an otherwise representable page as overflowed. Focused near-four-GiB logical-capacity, COW and recovery cases are included in automated validation; fixtures do not allocate huge native pages. These boundaries follow Ghostty Screen.appendGrapheme, startHyperlink, cursorSetHyperlink and Terminal.print; Windows Terminal and xterm.js have different storage models.
Page replacement now distinguishes a failed cell clone from cursor-only style reinsertion failure. Like Screen.increaseCapacity, it commits the successful clone, defaults a refused cursor pen, and independently attempts hyperlink restoration once. A COW-owned notification updates active/dormant pen registers, including snapshot capture and later input, without discarding cell styles or protection. Explicit later SGR may establish a new pen. Deterministic crowded-probe regressions cover grapheme/link-driven growth, independent cursor details, saved cursors and COW ownership without forcing process OOM. These tests are included in automated validation; no new performance claim is made.
Exhausted cross-page row-copy retries are now fatal to the managed terminal owner, rather than falling through as a successful copy of the source payload. Ghostty's Screen.clonePartialRowGrowCapacity panics here because callers have already moved rows and pins. RoyalTerminal deliberately contains the failure to the screen/processor instead of crashing its embedding process: further input, reset/session reuse, resize, search, selection and snapshot/continuation export reject the partial state. A fresh processor and screen are required. COW readers retain their independent state, and failed synchronized-output staging is never published by timeout or disposal. Cursor accounting skips exception unwinding; the existing output worker propagates the failure and stops its drain. Successful capacity growth still retries the copy. Twenty-four full-width/rectangular, grapheme/style/link-map/string-pressure, COW, lifecycle, held-publication and worker regression cases are included in automated validation.
CLR allocation failures in scalar/batched printing, grapheme append/transfer, character shifts, row/cell erasure, hidden-cell retirement, raster text clearing, prompt redraw and cursor/row metadata coordination now latch the affected owner before exception cleanup. The latch stores the original exception without allocating a tracker or ExceptionDispatchInfo; retained COW owners remain independent, and copying a failed owner preserves its failure. Exception filters run before row-revision stamping and cursor restoration, so cleanup cannot mark partially updated metadata synchronized or hide the first failure. Native logical capacity refusals still retain their existing retry/degradation behavior. This is failure containment, not a promise of successful recovery from process OOM.
These are internal mutation boundaries, not a catch around arbitrary input: title/bell/response observers may fail after a valid mutation, and a failed staged resize must still roll back to its usable original owner. Instance-local allocation tripwires cover partial metadata edits, held-output nonpublication, revision cleanup, copied failures and resize rollback; those new tests are included in automated validation. These tests exercise the supported owner-fault/publication policy; exhaustive failure equivalence is not claimed.
Character insertion/deletion now reuse one writable span per row, avoiding repeated COW checks and metadata revision increments per shifted cell. Printing, erasure and raster clears detach writable cell storage before committing metadata where no full-row ownership swap is needed. Full clears retain their no-copy replacement path. There are no per-cell journals, delegates or rollback snapshots on the normal path. This ordering follows the prepare-before-install approach in Windows Terminal ROW::_resizeChars and the separate cell/combined-string storage in xterm.js BufferLine, while preserving Ghostty's page pressure and partial-append semantics. The --managed-print benchmark now includes tracked/untracked character shifts, tracked erase and grapheme append for final before/after measurement. No measured speedup or current-head CI result is claimed in this implementation phase.
Synchronized-output preparation now stages the complete owner and deadline, and reserves mode-table capacity, before exposing either a held frame or enabled mode 2026. Failed preparation retains the visible owner; failed pre-publication work retains the old hold/deadline and mode for retry. Timeout and protocol release use the same commit ordering. A source or destination with a latched mutation failure cannot participate in ownership transfer. Partial quota enforcement and native or host history retirement latch the affected owner before cleanup; a failed private quota update cannot publish on input, timeout or disposal. Dormant-buffer scopes prepare missing raster collections before changing live fields, since a throwing constructor would not run their restore logic.
History prepend keeps all decoding, hyperlink registrations, anchor arithmetic and prompt inspection before row publication, like Ghostty PageList.PageAllocation.prepend. Empty raster lists are reused rather than allocated for every unlinked history page. State/history commit tails mark dirty rows under their existing serialized-access contract, without entering another monitor after ownership transfer. COW page forks and same-identity reflow tables use ConditionalWeakTable.AddOrUpdate instead of removing/re-adding weak entries; new page identities are registered before old entries are retired. Tail slot bookkeeping is reserved before consuming a free slot. These changes reduce unnecessary registry churn and commit-time work, not the required COW snapshots.
The chosen visibility behavior follows Ghostty's renderer suspension and timer, Windows Terminal's synchronized-output mode and xterm.js's buffered refresh/timeout. The managed host additionally needs explicit COW publication safety because its published rows are shared with independent readers. Twenty publication/quota/ history/ownership failure and retry cases are included in automated validation. The --managed-print benchmark adds tracked/untracked hold cycles; final build, test, benchmark and CI results are recorded below. This is not yet a completed audit of every allocator failure or platform path.
Screen-only buffer switching now prepares missing rows and raster collections before publishing the selected buffer, following Ghostty ScreenSet.getInit and switchTo, Windows Terminal's alternate-buffer creation and xterm.js's fill-before-activation ordering. The existing Ghostty-compatible persistent alternate-buffer policy is unchanged. Failed screen-only preparation remains retryable; failure after selection or within a protocol switch faults the owner, since protocol mode/cursor registers may already have changed. Row recycling, multi-row scroll-clear, theme recoloring and raster-anchor shifts also contain partial allocation failures. A failed owner cannot be repaired by clearing all rows. A normal screen reset prepares replacement rows directly without normalizing a dormant buffer only to discard it.
Resize failures latch their private staging owner before cursor-lease cleanup. Faulted leases do not fork shared tables during unwinding, preserving the original failure and the outer transaction's usable pre-resize owner. Successful and non-faulted abandoned leases retain their existing reference-restoration rules.
Raster-image retirement skips registry scans when scrolling changes anchors but removes no placements. Empty/single-image cleanup needs no scratch allocation; multi-image cleanup reuses owner-local scratch and removes stale dictionary entries without a temporary deletion list. This scratch is not snapshot state and is not shared by COW owners. Replacement reserves registry/list capacity before deleting overlap or clearing text. Thirty structural/cursor/raster regression cases and a --managed-raster benchmark (1/2/8/64 live images, warmed replacement, seven samples) are included in automated validation. Expected savings are fewer temporary collections and unnecessary source scans, not a measured throughput claim; execution and current-build measurements are recorded below; no historic before/after ratio is inferred.
Snapshot metadata coordination now reuses two bounded row-group buffers and one retained-slot set per tracker. Leases clear row references on both success and failure; scratch is neither copied nor shared with independent COW owners. Page synchronization skips metadata-map rescans only after its first full reconciliation and only when every live physical slot has the expected revision. Restored seeds still discard metadata outside installed rows; dirty non-cursor rows, changed slot membership and interleaved pages retain their full reconciliation path. Grouping still scans the row buffer: this is not constant-time page lookup. Style retirement also enumerates removable dictionary chunks directly instead of allocating a copy of the chunk keys.
Ghostty Screen.cursorChangePin retains same-page state and migrates references across pages; its RefCountedSet recycles entries in page storage. The managed reference set now keeps one cleared entry wrapper for tail trimming, preferred-ID replacement and dead-bucket reuse. Deletion callbacks, IDs, probe/backshift order, native rehash/capacity refusals and reference counts are unchanged. The spare keeps no encoded hyperlink payload and is not copied into another allocator. This is bounded CLR reuse, not a dense allocation from snapshot capacity hints. Windows Terminal's ROW and xterm.js's BufferLine remain row-storage references, not definitions of Ghostty's PAGE allocation contract.
Twenty-two new revision/retirement, allocation, seed, COW, collision and weak-owner lifetime cases are included in automated validation. --managed-print adds isolated snapshot pen changes and 128-row cross-page cursor cycles. Expected benefits are fewer temporary collections, metadata scans and per-style wrapper allocations; timings, allocation measurements and regression execution are included in automated validation.
Full-width IL/DL now clamp the count to the affected rows and traverse once in the native direction, copying directly from the requested distance. In-place SU/SD share this bounded movement; the primary top-origin history path still creates rows individually. Discarded intermediate rows no longer cause needless metadata growth or a fatal copy when the command only needs to clear the region. Tracked cell and raster anchors move by the bounded delta; the existing Kitty margin wrapper retains stationary IL/DL placements and scroll-specific clipping. Effective IL/DL reset pending wrap and return to the left margin, while no-ops outside either margin axis preserve both. Wide-cell inspection now uses read-only references, keeping valid ASCII/wide rows shared with COW readers until a real repair or erase. Ghostty's Terminal.insertLines/deleteLines defines traversal, allocation and wrap semantics; Windows Terminal's _InsertDeleteLineHelper also scrolls a bounded rectangle, whereas xterm.js uses repeated line splices and has no Ghostty PAGE allocator. Forty-two new large-count, discarded-metadata, pressure, COW, anchor/raster and pending-wrap cases are included in automated native-comparison coverage. The expected gain is one row traversal instead of one traversal per requested line; no measured speedup is claimed before profiling.
Scrolling now distinguishes explicit SU/SD from LF/IND. SU/SD account for the temporary top/bottom cursor visit and its return, including unprinted style allocation, implicit hyperlink reissue and native pen/link degradation. Primary history growth also accounts for each intermediate page before painting its background. Full-width LF/IND and alternate full-screen SU rotate row ownership within each logical page; only boundary rows are cloned. Surviving wrap flags, wide spacers, semantic metadata and COW cell storage remain intact, while the recycled row loses old metadata and takes the accepted background. Page identities and physical allocator slots remain owned, with no history created by alternate rotation. The cursor remains on its page during bounded LF/IND; row rotation does not create a spurious hyperlink migration. Fatal boundary copies retain the existing owner-fault/no-publication behavior. Reverse index preserves pending wrap when it scrolls, but clears it on its cursor-up fallback, including a clamped row-zero move. Forty additional cursor, rotation, COW, background, single-row and native differential cases are included in automated validation. These decisions follow Ghostty Terminal.scrollUp/scrollDown/index, Screen.cursorScrollRegionUp and PageList.eraseRow[Bounded]; Windows Terminal rectangle scrolling and xterm.js line splices do not supply the per-page cursor contract. No throughput or allocation benchmark claim is made yet.
Post-rotation page growth/rehash and checkpoint replacement now clone metadata in logical row/column order and rebase physical slots together with revision coverage and the tail watermark. Grapheme slices and hyperlink strings are repacked in that order; style references retain preferred-ID and inline-background observations. A failed clone keeps the old mapping. In-flight cell writes, cursor hyperlink-map retries and bulk reconciliation resolve their addresses again after every successful replacement. COW publications keep their original allocator/slot pairs without detaching unchanged cell arrays. Translation is sparse in retained rows and occupied metadata; style traversal keeps compact chunks rather than a per-cell sort. Source and destination strides are independent and the existing host hidden-column policy is retained. These decisions follow Ghostty Page.cloneFrom/clonePartialRowFrom; WT ROW and xterm.js BufferLine do not define its page allocator contract. Forty additional mapping, retry, failure, COW, tail-reuse, huge-hint and native continuation cases pass in the automated suites. Top-origin history scrolling with a bottom margin now also rotates within each page. It first appends the tail and migrates the cursor to its next logical row, then visits the stationary suffix tail-first, cloning only the rows that cross page boundaries. It clears the recycled cursor row after those copies. This preserves contiguous allocation ownership, physical slots and the native order of cursor-link migration, metadata growth and copy retries. Unchanged same-page cell arrays remain shared with COW readers. Host anchors and raster placements below the margin shift once after any history pruning; held output publishes the rows and allocators together. Exhausted boundary copies use the existing fatal owner/no-publication contract. Cursor-page entry also retains the incoming hyperlink's original identity through style installation: if that grows or rehashes the destination page, the link is restored once before the style retry and normal link migration. Refusal drops only the link, without growing its tables or consuming an implicit ID; successful restoration is released by the subsequent migration. This also applies to ordinary cursor movement, with explicit/implicit links and provisioned/empty destination storage. Ghostty Screen.cursorScrollAboveRotate defines the page contract; Windows Terminal pans then scrolls the suffix, and xterm.js BufferService.scroll inserts a line without page-local allocators. New ownership, pruning, COW, hold, pressure/failure and native continuation cases cover fresh and reused tails. The final review covers the implemented mutation, publication and failure boundaries; exhaustive allocator-failure equivalence is not claimed.
Unrepresentable allocation state rejects additional history rather than wrapping a capacity or undercharging it; quota eviction can remove such a page only once it is wholly historical. A subsequent representable content checkpoint can recover admission. Builds and native comparisons are covered by automated validation; isolated performance results do not establish whole-application throughput.
Managed bitmap searches safely reject an oversized span at the last word rather than reading past the bitmap. A hash-checked native correctness overlay now applies the same end check to the pinned allocator, without changing its allocation order or consuming bits on failure. Native/managed regressions cover oversized URI and explicit-ID spans, occupied prefixes and complete reuse after failure; the existing native overlay is included in the validated builds. Windows Terminal and xterm.js do not use this native PAGE contract, so Ghostty defines these restore decisions. Golden hash vectors, bitmap/set lifecycle cases, adversarial hash collisions, seeded native comparisons and post-restore overwrite tests are included in automated validation. Event-level accounting and pressure-driven splitting for live mutations are implemented through the owner-local page tracker described above.
Font thickening
TerminalControl.FontThicken enables macOS CoreText font smoothing for terminal text and IME preedit, for either VT engine. FontThickenStrength ranges from 0 (lightest smoothing) to 255 (strongest); it has no effect while thickening is disabled. These values are also available as Thicken and ThickenStrength in TerminalFontRenderingSettings and are persisted in appearance profiles.
This follows Ghostty's macOS smoothing path: grayscale CoreText masks, linear-gray color space, a strength-controlled gray drawing color, and padded glyph bounds. It is independent of the existing FontEmbolden synthetic-bold setting. Shaping, cell advances and colors remain controlled by RoyalTerminal. Color-font glyphs retain the Skia renderer, and other operating systems retain normal rendering, matching Ghostty's platform support for this setting. Glyph and font caches are bounded, invalidated when font settings change, and disposed with the renderer.
Glyph failure boundaries and replies
Managed Glyph Protocol buffering now follows the pinned Ghostty APC handler: allocation failure or the 1 MiB body limit abandons the whole recognized command until exit, silently, without executing a retained prefix or reporting it as an unknown APC. An oversized bulk slice is rejected before reserving body storage. Small command scratch is reused up to 4096 bytes; larger scratch is released on completion, cancellation or rejection. Repeated large commands may consequently allocate more than the old permanently retained list; idle retention is bounded.
Registration allocation failures during base64 scratch, outline/entry creation, lazy glossary creation or FIFO reservation return out_of_memory, respecting reply=0/1/2. Both dictionary and FIFO capacity are reserved before insertion, replacement ordering or eviction; failure leaves old entries/order intact. Existing-key replacement needs no reservation. Read-only queries and clear on an empty glossary no longer construct storage. Malformed registration codepoints report zero, rather than a partially parsed numeric prefix.
Execution returns a value-type result before delivering replies. Register/clear requests publish their glyph revision before response formatting or host calls, including rejected mutations. Host callback exceptions propagate and are not reclassified as glyph-storage failures; owned response-array allocation can also still fail. Glyph replies now use bounded stack byte formatting and one owned callback array, with no intermediate response strings. Coverage augmentation uses the same formatter in both backends. No-consumer or suppressed replies skip formatting and coverage lookup entirely.
These boundaries follow Ghostty's APC handler, glyph executor, FIFO glossary and Terminal.glyphProtocol dirty-before-effect ordering. xterm.js APC handlers also reject oversized captured commands, but do not define Ghostty's glyph FIFO or error replies. Windows Terminal dispatch does not supply this Glyph Protocol contract. The pinned Ghostty behavior is the reference; managed small-buffer reuse is an explicit allocation strategy choice.
Fifty-one new cases cover fault stages, verbosity, FIFO eviction/replacement, parser recovery, bounded retention, observer ordering, lazy empty storage and byte-for-byte reply formatting/ownership. --glyph-replies adds six paired string-versus-byte encoding workloads. Tests and the benchmark harness pass; reduced temporary allocation is not an end-to-end performance claim.
Graphics scene publication
The shared screen prepares Kitty placements, placeholder lookup and image registry before replacing any live scene state. Preparation failure keeps the previous frame, caches and copy-on-write readers intact. Image registry staging reuses two dictionaries; after each successful swap the old registry is cleared immediately, retaining capacity but no old pixel references. This scratch belongs to one screen instance and is not copied or transferred as transaction state. Pixel-only updates preserve projection caches and should allocate nothing after warm-up, at the cost of filling a second registry rather than updating live keys. Full geometry changes still allocate immutable placement/scene data.
Native image cache/generation state commits only after screen publication; failed geometry-only refreshes remain retryable even with an unchanged native generation. The managed adapter retains pending publication across failed preparation and resize rollback, retrying before subsequent input, on timed refresh outside a hold, or before releasing a held screen. Placeholder runs and projected geometry also commit together: a failed projection cannot cache new runs with old geometry and cause subsequent renders to skip the update.
Raster snapshot copying reserves dictionary/list capacity before replacing live contents, preserves per-buffer collection aliases and treats self-copy as a no-op. Allocation failure before publication is recoverable; a failure while clearing covered terminal cells retains the existing owner-local mutation-fault policy. This is not a claim that an entire input batch, protocol graphics store or terminal snapshot is rolled back when rendering publication fails.
The reference is Ghostty image storage, which reserves registry capacity before eviction/replacement. Windows Terminal ImageSlice prepares a resized pixel buffer before moving it into storage; its geometry update order is not adopted as a transaction guarantee. xterm.js image storage uses canvas/Map lifetimes and eviction, not this CLR render-scene ownership model. RoyalTerminal uses a staged scene boundary for shared native/managed rendering.
Twenty focused cases are authored for preparation/projection failures, retry, pixel-only scratch reuse, COW readers, raster ownership, hold release and resize rollback, including both backend adapters. --graphics-publication measures six full-scene/pixel-only workloads (1/32/256 images); the identical harness can run against pre-change b340f5a1 for a baseline. Tests and the current-build harness pass. Closeout results below record paired allocation and latency trade-offs; they are not a historical-build or end-to-end comparison.
Kitty image storage
The managed engine retains raw RGB images at three bytes per pixel, matching Ghostty's protocol storage. RGBA and decoded PNG use four. Rendering creates one cached, owned RGBA view without changing the image's storage charge or eviction generation; geometry-only reads do not expand pixels. Frame edits and composition use copy-on-write, so an earlier render publication keeps its original pixels.
BasicVtProcessorOptions.KittyGraphicsStorageLimitBytes is an admission budget, not a bound on all process memory. Like Ghostty, animation composition promotes the root to RGBA before resolving a new frame's base or reserving its full canvas. Promotion and existing-frame edits are quota-exempt, so retained bytes can exceed the admission limit. Subsequent image/frame admission reclaims actual bytes using transient/placement priority and generation order, excluding the animation target. An append may fail after evicting other images; a deficit larger than the limit is rejected before eviction. Frame deletion and retransmission release their stored bytes. The native build includes a hash-checked correction to an eviction assertion that previously excluded valid over-budget RGB promotion.
The separate KittyGraphicsMaxImageBytes safety setting bounds loaded/decompressed data and the largest RGBA view before allocation. Renderer caches, temporary load buffers and caller-retained publications are not charged to protocol storage. Unrendered RGB payloads retain three rather than four bytes per pixel; rendered RGB images can hold both source and cached view. No whole-application memory or speed improvement is claimed. Allocation, ownership, quota and cross-engine tests pass in the automated suites. The closeout benchmarks do not establish whole-application memory or speed improvements.
Kitty APC control fields are parsed incrementally into a fixed-size table and an eleven-byte temporary field, following Ghostty rather than xterm.js's fixed header-length cap. KittyGraphicsMaxApcBytes counts only encoded payload, not the G identifier or control fields. Zero still permits control-only operations. Payload capacity cannot exceed the configured bound; completion decodes in place and transfers ownership. Failed/disabled commands discard subsequent bytes without retaining a large APC buffer. Rejected commands neither cancel a previously accepted chunked image nor change its quiet policy. Normal APC exit/cancellation, reset and parser-continuation behavior are preserved. Focused split-input, resource and ownership tests are added; execution and allocation measurements are recorded in the validation results below.
Image-loader allocation failures now follow Ghostty's execution error and transfer lifecycle and reserve-before-append loading. Loader creation, owned-buffer copies/growth, bounded inflation, host medium/PNG loading and final pixel wrappers report ENOMEM: out of memory. Failed appends preserve accumulated bytes and retain a nonzero quiet override, including failed final chunks; clients can retry. Final decoding consumes the pending image/frame before it can fail. Queries do not consume a pending transmission, while explicit image retransmission still retires the previous image and placements at its start. Unrelated exceptions and response-observer failures are not swallowed. This is a loader-only error boundary, not a guarantee for later store/animation mutations or graphics publication.
The accumulation buffer is stored inline in its single-owner loader, eliminating one buffer-wrapper object per load and another during inflation. Payload adoption, bounded geometric growth, exact-size transfer, RGB retention and scratch-pool cleanup remain unchanged. Active buffer values must not be copied except to move ownership. The closest xterm.js image store and Windows Terminal image slices do not define Ghostty's pending Kitty transfer/quiet/error contract; we follow the pinned native implementation here. Sixty-two new failure/ownership cases and six --kitty-image-loader workloads are included in automated validation. The identical loader harness can run against 317610d3 for before/after profiling in final validation; reduced object churn is expected, not a measured throughput or working-set claim.
Image-store admission separately follows Ghostty's reserve-before-evict/replacement contract: prepare dictionary capacity and all managed image/initial-animation owners before evicting images or removing a replaced image's placements. Existing image and placement keys reuse their dictionary slots. Placement preparation similarly reserves capacity before replacement and releases a newly tracked anchor on any preparation failure. Allocation failures return ENOMEM, except tracked-anchor preparation uses native EINVAL: failed to prepare terminal state; unrelated exceptions propagate after releasing that temporary ownership. Opportunistic pruning and explicit retransmission's earlier deletion remain native-compatible side effects, not rolled-back transactions. A successfully admitted image remains available when its subsequent display fails. The eviction scan enumerates dictionary entries directly to avoid a first-use value-collection allocation after preparation. Twenty-six focused admission, replacement, relative/virtual placement, anchor, retained-screen and processor retry cases are included in automated validation. Later animation editing and graphics publication remain separate failure boundaries. No measured performance improvement or complete allocation-failure parity is claimed.
Animation allocation boundaries follow the native frame execution/promotion/reservation order and pixel conversion/composition. Source RGB conversion and root promotion fail with ENOMEM before assigning a frame response number; subsequent canvas/list failures retain the resolved number. Successful promotion remains charged and changes the image generation even if later composition fails. In particular, a failed managed COW canvas must not leave the store charging its old RGB byte count. Quota-driven evictions before an append are retained, but an unstored canvas is never charged. Metadata capacity is reserved before allocating/copying its canvas; frame pixels, gaps and playback timestamps commit only after all preparation succeeds. The managed COW requirement deliberately differs from native in-place frame edits because held render/snapshot readers share immutable pixels. Response observers and publication remain outside protocol-error catches; unrelated exceptions propagate with committed promotion accounting intact.
RGB expansion now uses a bounded four-pixel Vector128 shuffle with a scalar tail and fallback, opaque source-over pixels use direct copies, and nonzero backgrounds use the runtime's word-span fill. Full-canvas overwrites clone their owned source once without first copying/filling a canvas that would immediately be overwritten. Clipped rectangles and translucent pixels retain the native arithmetic. In particular, zero source alpha does not skip Wuffs' low-destination-alpha integer rounding. These are managed CPU equivalents, not transplants of xterm.js image storage or Windows Terminal image slices, whose storage/rendering paths do not define this Kitty animation contract. Sixty-eight new failure, ownership, alignment/tail and pixel cases are authored, including all 65,536 source/destination-alpha pairs in one reference comparison. --kitty-animation adds eighteen workloads; the same harness runs against bb6d33ae for historical before/after profiling. Automated execution, software-fallback coverage and current-build measurements are recorded below; interactive sign-off and exhaustive mutation-failure parity are not claimed.
Kitty parser state is now stored by value in the processor, removing a separate heap parser owner for each APC while retaining one independently owned command. Active parser values must not be copied; helper calls take them by reference. Command-object or payload-growth allocation failure silently discards that APC and releases its payload, matching the native handler's ignore transition. It does not modify an earlier accepted image transfer, its quiet policy or its pending frame target. Completion still decodes in place and transfers the buffer once, without a second payload allocation. Disabled Kitty creates no command. Only storage-allocation failures are caught; unrelated exceptions and host reply callback failures retain their normal propagation.
Managed Kitty responses now format the bounded numeric header in 64 stack bytes and encode the message directly into one owned byte array. Quiet/no-ID replies and absent consumers skip formatting. Image ID/number, placement and frame ordering remain unchanged; host-supplied messages retain their previous ASCII replacement policy and are not truncated to the header buffer's size. This follows Ghostty's parser and response encoding and stream effect guard. As for the glyph path, xterm.js APC capture rejects bounded handler payloads but does not define the same owned CLR command or retained native image-transmission state. Windows Terminal's dispatch does not provide the Kitty response/storage contract; pinned Ghostty remains the reference.
Thirty-six new failure/ownership/formatting cases are authored, including rejected continuation/retransmission quiet inheritance, all scalar-field maxima and parser owner allocation. --kitty-parser adds four ingestion workloads; the identical harness can run against pre-change 2f5c325f for its heap-parser baseline. --kitty-replies adds six paired StringBuilder/byte-encoding workloads. Automated tests and isolated benchmark workloads cover these changes; fewer parser/response temporary objects are expected, but measured end-to-end gains remain unproven.
Graphics payload decoding follows Ghostty's default simdutf forgiving-base64 policy: optional final padding, ignored ASCII whitespace and unused final bits, but rejected misplaced/excess padding and invalid alphabet characters. Full groups use the runtime's in-place decoder; the short tail is decoded without an extra array. This is separate from strict clipboard decoding. Pinned Ghostty's scalar fallback has inconsistent whitespace/invalid-padding behavior; the managed engine deliberately follows the default decoder on every platform. Differential tests cover shared valid inputs on all native builds and whitespace on SIMD builds.
Animation ticks follow the native host's write-then-render order. Stop, gap edits, frame uploads and screen switches within one input write execute before its next tick; parser command boundaries do not independently advance playback or invalidate an upload's saved image generation. DECSET 2026 is an explicit exception: it ticks and publishes the completed prefix before freezing presentation. Releasing the hold lets the remainder of that write run before the next tick. Idle deadlines and external resize still refresh animations. Deleting an earlier frame preserves the displayed frame's identity and elapsed gap, including when it was the last frame; the reviewed native frame-deletion overlay now covers that case as well. Fake-clock cross-engine tests and a raw-native generation regression are included in automated validation.
Kitty drag and drop
Both VT adapters implement ITerminalDragDropTarget. A registered OSC 72 client receives drag movement/leave and drop messages, then requests MIME representations by index. Chunked registration and acceptance, multiplexer IDs, base64 data chunks, completion markers and BEL/ST responses follow the pinned Ghostty state machine. Registration survives RIS; new sessions unregister. Held data is released on conclusion, a new drag, cancellation, unregister or processor disposal.
Avalonia's composed drop behavior advertises plain text, file URI lists and supported typed platform MIME data. It captures representations only at drop time, without opening files or retrieving remote contents. The host offers copy only: its OS drag session finishes before the client concludes the asynchronous transfer, so it cannot safely promise a source-file move. The core contract still encodes copy/move operations for custom hosts. Unregistered drops remain available to the embedding application's handlers; no shell commands or unsolicited paste are generated.
Drops are bounded to 16 representations and 64 MiB. Like the pinned Ghostty core, remote transfer requests receive EINVAL, and unsupported drag-out receives EPERM; neither capability is advertised. Native host hooks are repository-owned extensions, not new upstream public C APIs. The native boundary copies all retained data and serializes with normal terminal mutations.
Kitty desktop notifications
Both VT adapters implement ITerminalNotificationSource; embedders can configure TerminalControl.NotificationHost with a nonblocking ITerminalNotificationHost. OSC 99 queries advertise only the backend's actual capabilities. Without a host, notifications and support queries are silently ignored. The default application now installs Linux freedesktop, macOS UserNotifications and Windows toast backends for live sessions, shared by both engines; capture replay never installs a desktop presenter. New platform implementations await runtime sign-off; this is not a claim of verified desktop delivery.
The shared protocol implements chunked title/body/buttons/icon assembly, strict safe UTF-8 and base64 input, application/type metadata, occasions, urgency, sound, replacement identities, activation/close/alive replies and monotonic expiry. Strings remain plain text, including literal markup. Encoded text preserves newlines/tabs and removes other control codes. Callback feedback is consumed only during serialized terminal refresh; it cannot write directly to the PTY from an OS callback thread. Superseded and prior-session callbacks are ignored. Effects remain responsive during synchronized-output holds without releasing the frame.
Limits are 64 unfinished and 64 active notifications, 4 MiB retained assembly and request payload budgets, 64 KiB per text field, 1 MiB per icon, 32 names/types or buttons, and a 128-entry/16 MiB session-local icon LRU. Individual OSC payloads follow Ghostty's 2048-byte plain / 4096-byte encoded limits; metadata is capped at 8192 bytes and IDs at 256 ASCII identifier characters. Input never becomes an arbitrary file path, shell command, or OS notification identity. Resource names are restricted to single identifiers resolved inside configured local XDG roots. Hosts must separately bound native image decoding and their own queues. RIS clears unfinished chunks; session changes, host replacement, detachment and disposal close owned revisions.
The Linux backend uses explicit D-Bus serialization and the negotiated desktop service capabilities; no reflection proxy, shell process or libnotify dependency. It binds calls/signals to a unique daemon owner, fails stale revisions on daemon restart and retries connection initialization. A per-window async worker owns at most 128 requests / 8 MiB of retained payload. Close is recorded as state even when admission is full. In-flight delivery finishes before cleanup so its returned OS ID can be closed; window closure waits asynchronously for this owned cleanup. Transport operations are bounded, and no desktop call waits under the VT lock.
Per-pane facades cache focus/visibility on the UI thread and route activation to the originating tab/pane. Queued focus is invalidated on session/host teardown. Bodies are escaped only when the server supports markup; literal title text is preserved, and servers without body support receive the body in the summary. Images decode only their first PNG/JPEG/GIF frame, with 1 MiB encoded, 2048-pixel per-axis and 1-megapixel decoded limits. All standard icon aliases and ordered custom names use local XDG themes, inheritance, size/scale selection and unthemed fallbacks. Locally installed desktop entries supply application icons; their commands are never executed and caller-supplied paths/URLs are rejected. Explicit names precede transmitted images; the application name is an implicit icon only when neither names nor image data is supplied. Icons are advertised when the daemon reports static or animated icon support; only the first frame is sent.
Theme roots follow XDG precedence, with GNOME GSettings, KDE and GTK configuration sources for theme selection and hicolor/freedesktop defaults. Optional GLib settings access uses explicit native imports, not GTK initialization or a shell. Theme documents, lookup work and caches are bounded; worker-side refresh observes file/theme changes on subsequent requests after five seconds. Standard and local sound names follow sound-theme inheritance, locale/profile and generic-name fallbacks, including user .disabled overrides. Both sound-name and resolved sound-file are supplied: the server's sound capability guarantees the latter, not the former. All standard sounds are advertised only when available locally (or explicitly disabled by the user); otherwise only system/silent are advertised.
Native Wayland activation-token integration is outside the supported current-version API boundary. Avalonia 12.1.1's native Wayland activation is a no-op and exposes no token-aware activation feature. Linux focus capability is therefore advertised only for an actual X11/XWayland window handle, not inferred from session environment variables. Native Wayland delivery/reporting remains usable without claiming that clicking a notification can focus the originating terminal.
New tests cover backend negotiation, replacement, early/stale signals, queue saturation, in-flight close/shutdown, image limits and headless pane lifecycle. Additional tests cover XDG root/name/theme order, desktop-entry masking, all icon and sound aliases, locale and disabled sounds, cache invalidation, bounded lookup, path traversal rejection and backend-specific focus capability. An isolated loopback D-Bus peer exercises actual message serialization without contacting the user's desktop bus. Automated coverage includes these loopback tests; real desktop-server policy is a separate release-environment check.
Reference decision: the pinned Ghostty OSC 99 implementation supplies a parser but no stream/desktop delivery. Windows Terminal's OSC dispatcher has no OSC 99 handler and xterm.js exposes host registration through registerOscHandler. RoyalTerminal follows the Kitty desktop notification protocol for the shared host lifecycle. Three reviewed native overlays expose the parsed command without adding an upstream action enum value. Callback, protocol and headless lifecycle tests are included in automated validation. Linux resource resolution follows the icon theme specification and sound theme specification.
The macOS presenter uses a separate universal arm64/x64 host bridge bundled in the Avalonia application package; it does not depend on the selected VT engine or add Ghostty VT exports. Apple UserNotifications requires a real .app bundle with a bundle identifier. Missing native assets, unbundled command-line hosts, denied authorization and a notification-center delegate owned by another embedding host leave support unavailable. Initialization and support queries never prompt for permission: only a real delivery can request alert/sound authorization.
Source-generated JSON carries bounded requests to a native per-client owner. Native completion blocks never call managed function pointers. An owned managed poller reads completion/activation and delivered-notification state, so callbacks from superseded requests or previous sessions cannot activate the current pane. Delivered replacements reuse their OS ID with a new revision token; a still-pending replacement gets a different ID so late cancellation cannot remove its successor. Categories retain other applications' registered categories and are removed on owner teardown. Actions preserve button indices; the shared protocol decides whether to focus the originating pane. The OS can still activate the application for its default action.
macOS icon names use standard system symbols or application bundle icons, in order, then bounded single-frame PNG data. Attachment preparation/file writes run off the AppKit queue and use generated private temporary directories, removed on completion or cancellation. Only system/silent sounds are advertised, matching the upstream macOS presenters. Low urgency is passive; normal/high preserve ordinary notification policy without requesting a critical-alert entitlement. All-close-event and full urgency capability flags are deliberately absent. Delivered-state polling provides an eventually refreshed alive cache (two missing observations after a delivery grace period); c=1 receives the protocol's untracked response.
Closing a pane cancels its outstanding delivery wait without stopping other panes; one native authorization wait serves the remaining requests without retaining the cancelled payloads. Window close cancels all waits and awaits native cleanup. An OS add completion that exceeds the bounded shutdown wait remains natively owned and removes its own late request, without retaining a managed callback or handle. Objective-C tests use a fake center for replacement/cancellation/delegate lifetimes; managed tests use a fake transport. The universal bridge build and native tests are wired into macOS CI and its artifact into cross-platform NuGet packaging. Automated builds and tests pass; package inspection applies only to the audited artifacts identified in the PR. Real permission/activation sign-off remains device-specific.
The Windows presenter uses a separate C++/WinRT host bridge for x64 and arm64, sharing the source-generated command transport, managed ownership and cancellation with macOS. Its native actor owns the COM apartment, notification objects, event subscriptions and local image files. No managed callbacks, reflection, PowerShell, Windows App SDK runtime, shell execution or remote image URLs are used. Disabled notification settings and unavailable native assets suppress advertised support.
Packaged hosts use their package identity. Unpackaged hosts register a per-user Start Menu shortcut with a stable executable-path-derived AUMID, following the desktop toast identity contract. The shortcut points only to the actual host executable, never to terminal data; an existing nonmatching shortcut is not overwritten. Generic dotnet/test hosts are excluded. The registration persists for Windows notification settings. It does not install a COM activator or URI protocol handler, and does not alter taskbar pins or the host's process-wide AppUserModelID. Installer-managed identity and interactive desktop behavior require platform sign-off.
The design follows Windows Terminal's DesktopNotification.cpp: activation callbacks belong to live toast objects, and tag/group identities support replacement. Each native client owns a unique group, so windows do not clear each other's toasts. Retired tokens cannot focus replacement sessions. XML is built through DOM text and attribute setters; opaque button arguments cannot contain terminal-provided commands. Windows supports at most five buttons; the first five retain their original indices. Ordered stock/application icon names use a fixed local shell namespace, followed by bounded normalized PNG input. Private, generated image files live only as long as the native toast lease. No arbitrary caller path is opened. System/silent sounds are supported; named sound support is not advertised. Low urgency suppresses the popup, normal uses default priority, and high requests high priority without bypassing Focus Assist or system notification policy.
Because Windows can launch a second host instance in addition to delivering an in-process activation, embedders should call TerminalNotificationLaunch.IsInertActivation(args) before creating their desktop lifetime, and exit for this inert sentinel. The demo entry point does this. Closing a notification cannot reopen a terminal or replay a command. Delivered-history polling maintains an eventually refreshed alive cache; full close-event support is not claimed. Shutdown revokes handlers, removes owned toasts and releases images. A stalled OS RPC can outlive the bounded managed wait under native-only ownership; the bridge remains loaded so eventual cleanup is safe.
Windows bridge source builds require Visual C++ build tools (x64 and ARM64) and the Windows SDK, via scripts/build-windows-notifications.ps1. CI builds both architectures and has a fake-platform native lifecycle harness plus cross-platform managed tests; these tests do not register shortcuts or show desktop notifications. CI and release packaging include both Windows DLLs and the universal macOS dylib. Build, test and packaging results are recorded below. Real Windows desktop activation, permissions and notification policy remain environment-specific release checks.
Closeout validation and performance
The current-version closeout was validated on macOS arm64 / .NET 10 Release on 2026-09-28, including the notification callback lifetime fix at feca1475. Dependency pins and existing native extension sources did not change. The complete three-platform CI baseline at adcdc666 predates that fix; current PR checks are authoritative for subsequent commits.
| Check | Result / scope |
|---|---|
| Local full suite | At feca1475: 7,733 unit/headless passes, 16 existing skips, zero failures; 259 required-native integration passes and six startup passes, no skips. |
| Software fallback | 128 checksum/grid/animation-failure tests pass with DOTNET_EnableHWIntrinsic=0, zero failures/skips. |
| Native ABI / generated colors | 161 ABI types and 27 callback signatures match; 782 generated X11 color names verified. |
| Build | Release solution passes; four existing TextBox.Watermark obsolescence warnings, zero errors. |
| Benchmark execution | All 38 named benchmark modes complete; source-defined warm-up/sample counts are retained. Checksum, grid and animation benchmarks also run with managed hardware intrinsics disabled. |
| Review | Current-version capability gating, native validation entry points, snapshot owner-local failure/retirement, and Kitty frame preparation/COW boundaries were reviewed. Disposing a native wrapper now unregisters its notification callback before a borrowed or leased terminal can outlive the managed delegate; both lifetime orders have regression coverage. Existing failure and native-differential assertions are retained. |
| Desktop qualification | Automated tests are not evidence of real IME/candidate placement, OS notification permission/activation, secure input or GPU/compositor certification on every platform. These environmental limitations are not implemented via private dependency hooks. |
The following are isolated medians from the existing harnesses. Setup is excluded where each harness says so; no whole-application/GPU speedup or historical before/after ratio is inferred from a current-build-only workload. Allocation counts are managed bytes, not native working-set measurements. Small workloads can trade reduced allocation for higher CPU time; those results are retained.
| Workload | Observed result |
|---|---|
| Image-row damage, 2,400 rows / 20 updates | Damage-limited 0.978 ms versus full invalidation 298.632 ms; both allocate 2,080 bytes. This compares two modes of the same software-Skia harness. |
| Kitty reply, maximum fields / 100,000 replies | Bounded encoder: 3.776 ms / 10.4 MB versus StringBuilder: 10.529 ms / 87.2 MB. |
| Kitty reply, image-only / 100,000 replies | Bounded encoder: 4.809 ms / 4.0 MB versus StringBuilder: 3.676 ms / 19.2 MB. Lower allocation, slower in this small case. |
| Snapshot metadata, 215 rows / 4,651 queries | Combined query: 2.151 ms versus three indexed queries: 8.348 ms; both allocate zero bytes. At one row the corresponding 1,000,000-query sample was slower: 28.977 versus 21.988 ms. |
| Graphics publication, 256 images / 2,000 updates | Pixel-only: 7.986 ms / zero bytes versus full-scene: 35.582 ms / 4.144 MB. At 32 images pixel-only was slower (8.646 versus 3.485 ms), while still allocating zero rather than 560 KB. |
| Kitty range deletion | All six 16/256/4,096-image cases allocate zero bytes during the measured 16-scene mutation loop. Scene/COW setup is excluded. |
| Search after history prepend | Only 128 rows scanned in all six 1,024/16,384-row cases. Result materialization still allocates in proportion to match count; it is not an allocation-free search claim. |
| Managed printing / 25,000 feeds | ASCII/Unicode/wide, styled, charset, edit, snapshot-pen and cross-page cursor workloads have zero measured allocation. Grapheme storage and synchronized-output COW workloads still allocate. |
| Snapshot palette / 1,000 installs, no overrides | Shared canonical/host palette: 72,000 bytes; owned palette: 1,432,000 bytes. These are different ownership workloads, not proof that all restores save the same amount. |
Reproduce using dotnet build RoyalTerminal.slnx -c Release, then dotnet run --project tests/RoyalTerminal.Benchmarks -c Release --no-build -- <mode>. The named modes and their fixture sizes live in the benchmark program; use identical harness inputs when comparing revisions. scripts/validate-macos.sh builds the shipping native integration and runs isolated Release batches, startup and required-native checks. --skip-native-build reuses an already-built artifact but still checks required RoyalTerminal exports. scripts/run-integration-tests.sh --skip-build also requires native availability and stages the validated library into test output.
Ghostty-compatible shaders
RoyalTerminal also supports a Ghostty/Shadertoy-style shader compatibility mode in the managed Skia renderer. This is intentionally separate from the native Ghostty VT binding and from Ghostty renderer interop.
Use TerminalShaderLanguage.GhosttyShadertoy when you have a single-pass mainImage shader that samples iChannel0:
using RoyalTerminal.Shaders;
Terminal.ShaderSources =
[
new TerminalShaderSource(
"Ghostty Compatible Shader",
ghosttyStyleSource,
TerminalShaderLanguage.GhosttyShadertoy)
];The shader runs as a RoyalTerminal framebuffer post-process effect, so it works regardless of whether the active VT engine is managed or Ghostty-backed. Native Ghostty renderer custom-shader injection is not part of the current interop path.
See Ghostty/Shadertoy Shader Compatibility for the supported uniforms, source shape, and limitations.
Start with the high-level wrappers
If you want Ghostty behavior inside RoyalTerminal, you usually begin with the managed wrapper types instead of the raw ABI mirror.
| Type | Purpose |
|---|---|
GhosttyTerminal | Managed lifetime wrapper for the native terminal. |
GhosttyRenderState | Managed wrapper for render-state extraction from the native terminal. |
GhosttyFormatterScreenOptions | Screen-level formatter options. |
GhosttyFormatterExtraOptions | Formatter extras for palette, modes, tabstops, keyboard state, and screen details. |
GhosttyFormatterOptions | High-level formatter request model. |
GhosttyFormatter | Formatter for exporting terminal state. |
GhosttyKeyEncoder | Native-backed key encoder. |
GhosttyKeyEvent | Mutable key event payload. |
GhosttyMouseEncoder | Native-backed mouse encoder. |
GhosttyMouseEvent | Mutable mouse event payload. |
GhosttyPaste | Static helper for paste encoding. |
GhosttySelection | Managed selection value used by formatters and helpers. |
GhosttySelectionGesture | Owned state machine for native text-selection gestures. |
GhosttySelectionGestureEvent | Reusable selection pointer/click event. |
GhosttyTrackedGridReference | Owned reference that follows a cell through scroll, pruning, and reflow. |
GhosttyColorUtilities | Native-backed color parsing, palettes, color math, X11 names, and color-scheme reports. |
GhosttyUnicode | Ghostty-exact codepoint and grapheme width helpers. |
GhosttyKittyGraphics | Managed helper for Kitty graphics extraction. |
GhosttyKittyGraphicsImage | Managed Kitty image snapshot. |
GhosttyKittyGraphicsPlacementIterator | Managed iterator over Kitty placements. |
GhosttySys | Static system helper surface. |
GhosttyVtHelpers | Static helper surface for protocol encoders and build info. |
GhosttyBuildInfoSnapshot | Full native build metadata snapshot. |
GhosttyBuildFeatures | Compact native build capability snapshot. |
TerminalBuffer | Managed helper for reading terminal content. |
TerminalDataProcessor | Static helper for processing terminal data through Ghostty-backed models. |
NativeLibraryLoader | Native library loader for libghostty-vt. |
These are the types used by RoyalTerminal itself when it wants native VT behavior without forcing consumers to work directly against raw pointers and C structs.
Engine-neutral effects and Unicode
Hosts that can use either VT engine should query the terminal contracts instead of depending directly on Ghostty types:
| Contract | Capability |
|---|---|
ITerminalEffectSource | Clipboard writes, desktop notifications, progress reports, and working-directory changes. |
ITerminalUnicodeWidthProvider | Codepoint width and first-grapheme width using the active engine's rules. |
GhosttyVtProcessor implements these contracts with the normalized libghostty callbacks and Ghostty Unicode tables. BasicVtProcessor implements the same contracts with managed OSC parsing and RoyalTerminal Unicode tables. The effect callbacks are policy boundaries: applications still decide whether clipboard writes and desktop notifications are allowed.
The raw VT mirror is also public
Under those wrappers, GhosttyVtNative exposes the Ghostty VT ABI directly. This is not the right layer for most applications, but it is the right layer for advanced interop, diagnostics, or custom wrappers.
At the current Ghostty revision, TerminalNew takes columns and rows directly. Configure the returned terminal through TerminalSet; the former GhosttyTerminalOptions constructor struct no longer exists.
Root VT exports
GhosttyVtNative, GhosttyResult, GhosttyVtKeyAction, GhosttyVtKey, GhosttyVtMods, GhosttyOscCommandType, GhosttyOscCommandData, GhosttySgrAttributeTag, GhosttySgrUnderline, GhosttySgrUnknown, GhosttySgrAttributeValue, GhosttySgrAttribute, GhosttyColorRgb
Core protocol and build-info exports
GhosttyString, GhosttyMode, GhosttyModeReportState, GhosttyOptimizeMode, GhosttyBuildInfoData, GhosttyFocusEvent, GhosttySizeReportStyle, GhosttySizeReportSize, GhosttyDeviceAttributesPrimary, GhosttyDeviceAttributesSecondary, GhosttyDeviceAttributesTertiary, GhosttyDeviceAttributes, GhosttyPointCoordinate, GhosttyPointTag, GhosttyPoint
Formatter exports
GhosttyFormatterFormat, GhosttyFormatterScreenExtra, GhosttyFormatterTerminalExtra, GhosttyFormatterTerminalOptions
Input and pointer exports
GhosttyKittyKeyFlags, GhosttyOptionAsAlt, GhosttyKeyEncoderOption, GhosttyMouseAction, GhosttyMouseButtonId, GhosttyMousePosition, GhosttyMouseTrackingMode, GhosttyMouseFormat, GhosttyMouseEncoderSize, GhosttyMouseEncoderOption
Kitty graphics exports
GhosttyKittyGraphicsData, GhosttyKittyGraphicsPlacementData, GhosttyKittyPlacementLayer, GhosttyKittyGraphicsPlacementIteratorOption, GhosttyKittyImageFormat, GhosttyKittyImageCompression, GhosttyKittyGraphicsImageData
Render-state exports
GhosttyRenderStateDirty, GhosttyRenderStateCursorVisualStyle, GhosttyRenderStateData, GhosttyRenderStateOption, GhosttyRenderStateRowData, GhosttyRenderStateRowOption, GhosttyRenderStateRowCellsData, GhosttyRenderStateColors, GhosttyRenderStateRowSelection, GhosttyBuffer
Screen exports
GhosttyStyleColorTag, GhosttyStyleColorValue, GhosttyStyleColor, GhosttyStyle, GhosttyGridRef, GhosttyCellContentTag, GhosttyCellWide, GhosttyCellSemanticContent, GhosttyCellData, GhosttyRowSemanticPrompt, GhosttyRowData
Selection, system, and terminal exports
GhosttySelectionRange, GhosttySelectionOrder, GhosttySelectionAdjust, GhosttySelectionGestureBehavior, GhosttySelectionGestureBehaviors, GhosttySelectionGestureGeometry, GhosttySelectionGestureAutoscroll, GhosttySelectionGestureData, GhosttySelectionGestureEventType, GhosttySelectionGestureEventOption, GhosttyAllocatorVtable, GhosttyAllocator, GhosttySysImage, GhosttySysDecodePngCallback, GhosttySysOption, GhosttyTerminalScrollViewportTag, GhosttyTerminalScrollViewportValue, GhosttyTerminalScrollViewport, GhosttyTerminalCompressionMode, GhosttyTerminalCompressionResult, GhosttyTerminalScreen, GhosttyTerminalScrollbar, GhosttyTerminalBellCallback, GhosttyTerminalWritePtyCallback, GhosttyTerminalTitleChangedCallback, GhosttyTerminalEnquiryCallback, GhosttyTerminalXtversionCallback, GhosttyTerminalSizeCallback, GhosttyTerminalColorSchemeCallback, GhosttyTerminalDeviceAttributesCallback, GhosttyTerminalPwdChangedCallback, GhosttyTerminalClipboardWriteCallback, GhosttyTerminalDesktopNotificationCallback, GhosttyTerminalProgressReportCallback, GhosttyTerminalOption, GhosttyTerminalData
Runtime enums, structs, and callbacks
The public Ghostty surface also includes the broader runtime enum and action/config mirror types that are not part of GhosttyVtNative itself.
Runtime enums
GhosttyPlatform, GhosttyClipboard, GhosttyClipboardRequest, GhosttyMouseState, GhosttyMouseButton, GhosttyMouseMomentum, GhosttyColorScheme, GhosttyMods, GhosttyBindingFlags, GhosttyInputAction, GhosttyKey, GhosttyInputTriggerTag, GhosttyBuildMode, GhosttyPointTag, GhosttyPointCoord, GhosttySurfaceContext, GhosttyTargetTag, GhosttySplitDirection, GhosttyGotoSplit, GhosttyGotoWindow, GhosttyResizeSplitDirection, GhosttyGotoTab, GhosttyFullscreen, GhosttyFloatWindow, GhosttySecureInput, GhosttyInspectorAction, GhosttyQuitTimer, GhosttyReadonly, GhosttyPromptTitle, GhosttyMouseShape, GhosttyMouseVisibility, GhosttyRendererHealth, GhosttyColorKind, GhosttyOpenUrlKind, GhosttyCloseTabMode, GhosttyProgressState, GhosttyQuickTerminalSizeTag, GhosttyKeyTableTag, GhosttyActionTag, GhosttyIpcTargetTag, GhosttyIpcActionTag
Runtime structs
GhosttyClipboardContent, GhosttyInputKey, GhosttyInputTriggerKey, GhosttyInputTrigger, GhosttyCommand, GhosttyInfo, GhosttyDiagnostic, GhosttyString, GhosttyText, GhosttyPoint, GhosttySelection, GhosttyEnvVar, GhosttyPlatformMacOS, GhosttyPlatformIOS, GhosttyPlatformUnion, GhosttySurfaceConfig, GhosttySurfaceSize, GhosttyConfigColor, GhosttyConfigColorList, GhosttyConfigCommandList, GhosttyConfigPalette, GhosttyQuickTerminalSizeValue, GhosttyQuickTerminalSize, GhosttyConfigQuickTerminalSize, GhosttyTargetUnion, GhosttyTarget, GhosttyResizeSplit, GhosttyMoveTab, GhosttySizeLimit, GhosttyInitialSize, GhosttyCellSize, GhosttyDesktopNotification, GhosttySetTitle, GhosttyPwd, GhosttyMouseOverLink, GhosttyKeySequence, GhosttyKeyTableActivate, GhosttyKeyTableValue, GhosttyKeyTable, GhosttyColorChange, GhosttyConfigChange, GhosttyReloadConfig, GhosttyOpenUrl, GhosttyChildExited, GhosttyProgressReport, GhosttyCommandFinished, GhosttyStartSearch, GhosttySearchTotal, GhosttySearchSelected, GhosttyScrollbar, GhosttyActionValue, GhosttyAction, GhosttyRuntimeConfig
Runtime delegates
GhosttyWakeupCallback, GhosttyActionCallback, GhosttyReadClipboardCallback, GhosttyConfirmReadClipboardCallback, GhosttyWriteClipboardCallback, GhosttyCloseSurfaceCallback
Which layer should you choose?
Use the layers in this order:
- stay in the main RoyalTerminal packages if all you need is a terminal control, VT processor, or renderer
- use the high-level GhosttySharp wrappers if you need native terminal behavior directly
- drop to
GhosttyVtNativeand the runtime mirror types only if you are building new interop or diagnostics on top of Ghostty itself
That keeps the common path small while still preserving the full native escape hatch for advanced consumers.