diff --git a/.github/workflows/build-ghostty.yml b/.github/workflows/build-ghostty.yml
deleted file mode 100644
index 0745eab..0000000
--- a/.github/workflows/build-ghostty.yml
+++ /dev/null
@@ -1,97 +0,0 @@
-name: Build Ghostty
-
-on:
- workflow_dispatch:
- push:
- paths:
- - "native/ghostty/**"
- - ".github/workflows/build-ghostty.yml"
- - "!native/ghostty/**/*.dll"
- - "!native/ghostty/**/*.so"
- - "!native/ghostty/**/*.dylib"
- - "!native/ghostty/**/*.sha256"
- pull_request:
- paths:
- - "native/ghostty/**"
- - ".github/workflows/build-ghostty.yml"
- - "!native/ghostty/**/*.dll"
- - "!native/ghostty/**/*.so"
- - "!native/ghostty/**/*.dylib"
- - "!native/ghostty/**/*.sha256"
-
-permissions:
- contents: read
-
-concurrency:
- group: build-ghostty-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- native:
- name: libghostty-vt ${{ matrix.rid }}
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - rid: win-x64
- os: windows-latest
- - rid: win-arm64
- os: windows-latest
- - rid: linux-x64
- os: ubuntu-24.04
- - rid: linux-arm64
- os: ubuntu-24.04
- - rid: osx-arm64
- os: macos-14
- - rid: osx-x64
- os: macos-14
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install llvm-strip
- if: runner.os == 'Linux'
- run: |
- sudo apt-get update
- sudo apt-get install --no-install-recommends llvm
-
- - name: Build libghostty-vt
- shell: pwsh
- run: >
- ./native/ghostty/Build-Ghostty.ps1
- -Rid ${{ matrix.rid }}
- -InstallZig
- -SkipHashCheck
-
- - name: Upload ${{ matrix.rid }}
- uses: actions/upload-artifact@v4
- with:
- name: libghostty-vt-${{ matrix.rid }}
- path: native/ghostty/${{ matrix.rid }}
- if-no-files-found: error
-
- package:
- name: Collect libghostty-vt artifacts
- needs: native
- runs-on: ubuntu-24.04
- steps:
- - name: Download RID artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: libghostty-vt-*
- path: artifacts/libghostty-vt
- merge-multiple: false
-
- - name: Write SHA-256 manifest
- run: |
- find artifacts/libghostty-vt -type f ! -name '*.sha256' -print0 |
- sort -z |
- xargs -0 sha256sum > artifacts/libghostty-vt/SHA256SUMS
-
- - name: Upload combined natives
- uses: actions/upload-artifact@v4
- with:
- name: libghostty-vt
- path: artifacts/libghostty-vt
- if-no-files-found: error
diff --git a/.github/workflows/build-terminal.yml b/.github/workflows/build-terminal.yml
index c1b750a..facdde3 100644
--- a/.github/workflows/build-terminal.yml
+++ b/.github/workflows/build-terminal.yml
@@ -6,11 +6,7 @@ on:
- "**"
tags:
- "v*"
- paths-ignore:
- - ".github/workflows/build-ghostty.yml"
pull_request:
- paths-ignore:
- - ".github/workflows/build-ghostty.yml"
workflow_dispatch:
inputs:
version:
@@ -142,12 +138,10 @@ jobs:
with:
path: |
artifacts/tools
- artifacts/ghostty-src
- native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
# Never restore native sources or reuse binaries built from different inputs.
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
@@ -199,7 +193,7 @@ jobs:
uses: actions/cache@v4
with:
path: artifacts/tools
- key: zig-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/Zig.ps1', 'native/ghostty/ghostty-upstream.json') }}
+ key: zig-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/Zig.ps1', 'native/zig.json') }}
- name: Build dt-pty-host for every Unix RID
shell: pwsh
@@ -233,7 +227,7 @@ jobs:
with:
path: |
native/noto-emoji/NotoColorEmoji.ttf
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
@@ -337,11 +331,9 @@ jobs:
with:
path: |
artifacts/tools
- artifacts/ghostty-src
- native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rid }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rid }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
@@ -378,11 +370,9 @@ jobs:
with:
path: |
artifacts/tools
- artifacts/ghostty-src
- native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
@@ -411,11 +401,9 @@ jobs:
with:
path: |
artifacts/tools
- artifacts/ghostty-src
- native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
@@ -460,11 +448,9 @@ jobs:
with:
path: |
artifacts/tools
- artifacts/ghostty-src
- native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
diff --git a/.github/workflows/test-iseberg.yml b/.github/workflows/test-iseberg.yml
index 478dd89..ee67666 100644
--- a/.github/workflows/test-iseberg.yml
+++ b/.github/workflows/test-iseberg.yml
@@ -57,11 +57,9 @@ jobs:
with:
path: |
artifacts/tools
- artifacts/ghostty-src
- native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
- key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
+ key: native-v3-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/zig.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
@@ -72,7 +70,7 @@ jobs:
shell: pwsh
run: ./scripts/Install-IsebergTestPowerShell.ps1
- # A native cache miss downloads Zig and compiles Ghostty; keep that out of the test budget.
+ # A native cache miss downloads Zig and compiles dt-pty-host; keep that out of the test budget.
- name: Restore native libraries
timeout-minutes: 30
shell: pwsh
diff --git a/.gitignore b/.gitignore
index ed49128..3467c3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,10 +27,6 @@ Thumbs.db
.DS_Store
## Native libraries built from source (see native/Restore-NativeLibraries.ps1)
-native/ghostty/**/*.dll
-native/ghostty/**/*.so
-native/ghostty/**/*.dylib
-native/ghostty/**/*.sha256
native/linux-pty/**/dt-pty-host
native/linux-pty/**/dt-pty-host.exe
native/noto-emoji/NotoColorEmoji.ttf
diff --git a/AGENTS.md b/AGENTS.md
index 4883235..e316ace 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -47,11 +47,11 @@ already the primary scripting language for Windows packaging in this repo
## Native helpers
- Linux/macOS PTY host: `native/linux-pty` (`dt-pty-host.c`)
-- Ghostty VT library: `native/ghostty`
- Windows Explorer/toast helpers: `native/windows-shell`
-`dotnet build` restores these for the host RID automatically
-(`-p:SkipNativeRestore=true` to skip).
+`dotnet build` restores `dt-pty-host` for Linux/macOS host RIDs automatically
+(Zig version pinned in `native/zig.json`; `-p:SkipNativeRestore=true` to
+skip). Windows builds have nothing to restore.
## Platform packaging entry points
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 911523e..3ffd6bb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -69,13 +69,13 @@ Do not point that tool at this repository.
## Native helpers
- Linux/macOS PTY host: `native/linux-pty` (`dt-pty-host.c`; Zig `cc` on Linux, Apple clang on macOS)
-- Ghostty VT library: `native/ghostty` (Zig build of pinned Ghostty)
- Windows Explorer/toast helpers: `native/windows-shell` (MSVC, gitignored `bin/`)
-`dotnet build` restores Ghostty and `dt-pty-host` for the host RID (Zig is
-downloaded into `artifacts/tools` on first use; a system `zig` of a different
-minor version than the pinned one is ignored, so it does not break the build). See
-[`native/ghostty/README.md`](native/ghostty/README.md). Pass
+`dotnet build` restores `dt-pty-host` for Linux/macOS host RIDs; Windows builds
+have nothing to restore. The Zig version is pinned in
+[`native/zig.json`](native/zig.json); Zig is downloaded into `artifacts/tools`
+on first use, and a system `zig` of a different minor version than the pinned
+one is ignored, so it does not break the build. Pass
`-p:SkipNativeRestore=true` to skip.
## macOS
@@ -88,7 +88,7 @@ dotnet test Devolutions.Terminal.slnx
pwsh scripts/Build-MacOsPackage.ps1 osx-arm64 2026.3.0 artifacts/packages
```
-`dotnet build` restores Ghostty and `dt-pty-host` for `osx-arm64` / `osx-x64`.
+`dotnet build` restores `dt-pty-host` for `osx-arm64` / `osx-x64`.
The PTY host is compiled with Apple clang against the macOS 13 SDK. App-bundle
packaging is Darwin-only; release builds are signed and notarized with a
Developer ID identity (see [docs/macos.md](docs/macos.md) and
diff --git a/Devolutions.Terminal.slnx b/Devolutions.Terminal.slnx
index 1e87ed0..e92445e 100644
--- a/Devolutions.Terminal.slnx
+++ b/Devolutions.Terminal.slnx
@@ -3,7 +3,6 @@
-
@@ -27,7 +26,6 @@
-
diff --git a/NOTICE.md b/NOTICE.md
index b699ac4..5e18580 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -17,11 +17,6 @@ See the license in the Cascadia Code repository.
Vendored under `native/noto-emoji/`. Licensed under the SIL Open Font License
1.1. See `native/noto-emoji/LICENSE`.
-## Ghostty (`libghostty-vt`)
-
-Pinned native VT engine binaries under `native/ghostty/`. See
-`native/ghostty/LICENSE` and `docs/ghostty-engine.md`.
-
## AppImage type-2 runtime
Linux AppImage packages embed a pinned type-2 runtime. See
diff --git a/README.md b/README.md
index 13b6c9f..9085b83 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ as a self-contained **NativeAOT** desktop app, and rendered with **Avalonia 12**
- **ConPTY** on Windows and a real **forkpty** transport on Linux and macOS
- Azure Cloud Shell for remote Azure sessions
-- selectable built-in or **Ghostty** VT engine
+- a built-in VT engine written in C#
- Windows Terminal-compatible `settings.json`, actions, keybindings, and `dt` CLI
- source-integrated **Iseberg** PowerShell ISE profiles, with execution in an
owned PowerShell subprocess and a NativeAOT desktop
@@ -104,10 +104,6 @@ windows is unavailable, and persisted layouts are neither restored nor saved.
It is the per-launch equivalent of Windows Terminal's
`compatibility.isolatedMode` setting, which Windows Terminal 1.23 removed.
-Set `"experimental.terminalEngine": "ghostty"` to use the pinned
-`libghostty-vt` engine globally. A profile can override it with `"builtin"` or
-`"ghostty"`. ConPTY remains the Windows process transport for both engines.
-
The compiled-XAML settings editor is documented in
[docs/settings-editor.md](docs/settings-editor.md).
@@ -115,7 +111,6 @@ The compiled-XAML settings editor is documented in
```
Devolutions.Terminal.Core VT parser + text buffer + terminal engine
-Devolutions.Terminal.Ghostty NativeAOT-safe libghostty-vt engine adapter
Devolutions.Terminal.Render Immutable plans + HarfBuzz/Skia glyph renderer
Devolutions.Terminal.Connection ConPTY + Linux PTY + Azure Cloud Shell + browser shell
Devolutions.Terminal.Settings Layered Windows Terminal-compatible JSON settings
diff --git a/docs/advanced-vt-protocols.md b/docs/advanced-vt-protocols.md
index 3668da8..b8cc369 100644
--- a/docs/advanced-vt-protocols.md
+++ b/docs/advanced-vt-protocols.md
@@ -3,9 +3,8 @@
> [!NOTE]
> Windows local sessions use the bundled Microsoft ConPTY runtime, including
> `OpenConsole.exe`, rather than the OS copy that filters Sixel on some Windows
-> builds. With the built-in engine, Sixel producers such as `Get-XKCD -Show`
-> can render inline images. Other transports must likewise preserve graphics
-> payloads; the optional Ghostty engine does not expose image resources.
+> builds. Sixel producers such as `Get-XKCD -Show` can therefore render inline
+> images. Other transports must likewise preserve graphics payloads.
`Devolutions.Terminal.Core` parses advanced string protocols without depending on Avalonia,
Skia, Win32, or an image codec. It exposes decoded Sixel pixels and bounded
@@ -140,7 +139,6 @@ delete or reset does not paint stale cached pixels.
- kitty cell/z-index/number-targeted deletes (`d=c/x/y/z/n`) — silent no-op;
the cursor for `f=100` images without `r` stays put because Core retains codec
bytes without decoding dimensions
-- Ghostty image projection (the pinned C ABI exposes no image resources)
These gaps avoid remote I/O and renderer/input dependencies while keeping every
implemented parser and downloadable resource path bounded.
diff --git a/docs/browser.md b/docs/browser.md
index 53b9a4f..04cb641 100644
--- a/docs/browser.md
+++ b/docs/browser.md
@@ -21,7 +21,6 @@ Devolutions Terminal can run as a static Avalonia WebAssembly app. This is a
| Desktop capability | Why it is out of scope |
| --- | --- |
| ConPTY / `forkpty` inside the page | No process or PTY APIs in the WASM sandbox. A loopback host process can own the PTY and bridge it over WebSocket. |
-| Ghostty (`libghostty-vt`) | Native library, not compiled to WASM |
| `dt` CLI, broker, multi-window | Separate OS processes and named endpoints |
| Package identity, toasts, jump lists, global hotkeys | Windows / desktop shell |
| Native file dialogs, unrestricted filesystem | Browser sandbox (MEMFS only) |
diff --git a/docs/ghostty-engine.md b/docs/ghostty-engine.md
deleted file mode 100644
index 988e111..0000000
--- a/docs/ghostty-engine.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Ghostty terminal engine
-
-The Windows host always uses ConPTY for local processes. The optional Ghostty
-mode replaces the VT parser/state engine, not the Windows pseudoconsole or the
-Avalonia renderer.
-
-Select it globally:
-
-```json
-{
- "experimental.terminalEngine": "ghostty"
-}
-```
-
-Or override a profile:
-
-```json
-{
- "profiles": {
- "list": [
- {
- "name": "PowerShell",
- "experimental.terminalEngine": "ghostty"
- }
- ]
- }
-}
-```
-
-`builtin` remains the default. New and restarted terminal sessions resolve the
-setting; an already running session keeps its current engine.
-
-## Native integration
-
-`Devolutions.Terminal.Ghostty` uses source-generated `LibraryImport` calls and safe handles.
-RID-specific `libghostty-vt` binaries are **built from source** on restore and
-copied into JIT, NativeAOT, and MSIX outputs. The ABI type manifest is validated
-at startup. See `native/ghostty/ghostty-upstream.json` and
-`native/Restore-NativeLibraries.ps1`.
-
-## Stack headroom
-
-The engine's P/Invoke calls execute on whatever thread the host is using —
-including the ConPTY read loop's thread-pool thread with the default ~1 MB stack
-reserve. Full libghostty (renderer included) has overflowed a 1 MB stack in
-other hosts; the VT-only `libghostty-vt` has a much smaller surface. A test
-(`EngineSurvivesAdversarialCorpusOnSmallStackThread`) feeds an adversarial
-corpus on a 256 KB-stack thread — 4x below the host default — to pin that
-headroom. If the pinned upstream changes and that test fails, route engine calls
-onto a dedicated thread with an explicit `maxStackSize`.
-
-## Current rendering boundary
-
-Ghostty owns VT parsing, modes, viewport state, resize/reflow, scrollback,
-terminal replies, titles, and bells. Its render-state API is projected into the
-shared immutable cell model consumed by the existing Skia renderer.
-
-Projection is lazy. A feed only marks the projection stale. The next read of
-`Buffer`, the cursor or a snapshot (normally once per frame) re-projects the
-rows that Ghostty's render state reports dirty, then cleans that state. Title
-and working-directory changes are still raised during the feed. See
-[performance benchmarks](performance-benchmarks.md) for the measured effect. This keeps
-Avalonia tabs, panes, accessibility, clipboard, selection, search, and window
-behavior common across engines.
-
-The pinned C ABI exposes no Sixel, OSC 1337, or ConEmu image resources. The
-engine excludes those image capability flags and emits deterministic unsupported
-diagnostics instead of pretending to project graphics or silently switching the
-selected engine.
diff --git a/docs/history/ghostty-comparison-benchmarks.md b/docs/history/ghostty-comparison-benchmarks.md
new file mode 100644
index 0000000..5f0295f
--- /dev/null
+++ b/docs/history/ghostty-comparison-benchmarks.md
@@ -0,0 +1,491 @@
+# Performance benchmarks vs Ghostty
+
+> **Historical record.** The optional Ghostty engine has been removed from
+> Devolutions Terminal; the builtin engine is now the only one. The numbers
+> below were measured against `libghostty-vt` pinned at commit
+> `3c1ef5b32fc5ea6b93d28493fabf193f595139cf`. The `ghostty-vt` and `ghostty`
+> bench targets, the equivalence gate, `--no-gate`, `-RequireBeatGhostty`,
+> `-GhosttyBench` and `native/ghostty/` no longer exist, so the commands on
+> this page will not run as written. For the current benchmark, see
+> [performance benchmarks](../performance-benchmarks.md).
+
+This page explains how Ghostty backs up its "fastest terminal" positioning,
+what its own benchmarks do and do not measure, and how DT benchmarks its
+**builtin** engine and renderer against it. The goal is a claim that holds up
+under review: "with the default engine, DT matches or beats Ghostty at
+workload X, measured this way, on this machine."
+
+For renderer-only details (caches, pixel verification, historical numbers)
+see [renderer benchmarks](../renderer-benchmarks.md).
+
+## How Ghostty supports its performance claims
+
+Ghostty's public claims are mostly qualitative. Few of them come with a method
+someone else could rerun.
+
+| Claim | Where | Evidence |
+| --- | --- | --- |
+| "Same performance category" as the fastest terminals; "~100x faster than Terminal.app and iTerm" | README | None. Ghostty's own devlog measured about 6x against iTerm2 and under 2x against Terminal.app |
+| `cat` of plain text 4x faster than iTerm/Kitty, ~60 fps under load | v1.0 README | No method given. The README itself says "we need better benchmarks" |
+| SIMD parser: 7.3x faster ASCII parse, 16.6x faster UTF-8 decode; `time cat` of a 5.4 MB Japanese text: Ghostty 73 ms, Alacritty 66 ms, Kitty 392 ms, iTerm2 470 ms | [Devlog 006](https://mitchellh.com/writing/ghostty-devlog-006) | **The only write-up with a real method**: hyperfine microbenchmarks with noop/memcpy floors, plus 10-run `time cat` averages. Alacritty beat Ghostty in that table |
+| 480–500 fps in DOOM-fire | [Zig patterns talk](https://mitchellh.com/writing/ghostty-and-useful-zig-patterns) | Anecdote. The same talk says input latency was never measured |
+| 1.3: IO 20–30% faster on a 4 GB asciinema replay; frame builds 2–5x faster | [1.3.0 notes](https://ghostty.org/docs/install/release-notes/1-3-0) | Real-world replay, but the corpus is not public |
+| `cat` of 150 MB ASCII in 575 ms (nightly) | Social post, second-hand | One run, no hardware listed. Unverified |
+
+The [about page](https://ghostty.org/docs/about) itself is careful: Ghostty is
+"in some benchmarks faster, in others slower."
+
+### What Ghostty's in-tree benchmarks measure
+
+Ghostty's benchmarks live in `src/benchmark/` at the pinned commit (see
+`native/ghostty/ghostty-upstream.json`). They are built with
+`zig build -Demit-bench`, which produces two binaries:
+
+- `ghostty-bench +` runs one component microbenchmark. It prints
+ nothing; the documented method is to time the **whole process** with
+ `hyperfine --warmup N`. That time includes process startup and setup. This is
+ why many actions have a `noop` mode to subtract.
+- `ghostty-gen ` writes synthetic input: `ascii`, `utf8`, `osc`,
+ `kitty` and `styled`. Seeds are time-based by default, so you save the output
+ once and reuse the file.
+
+The actions that matter for a comparison with DT:
+
+| Action | What it times | Closest DT measurement |
+| --- | --- | --- |
+| `+terminal-stream --data=F` | Full VT parse and terminal state update, read in 64 KiB chunks | `vt` (`ghostty-vt` target) |
+| `+terminal-parser` | State machine only, actions discarded | none (DT's parser is not separable) |
+| `+screen-clone --mode=render\|render-clean\|render-partial` | Updates to the CPU `RenderState` while holding the lock | `render` snapshot/plan |
+| `+codepoint-width`, `+grapheme-break` | Unicode tables | none |
+| `+terminal-resize` | Reflow | none yet |
+
+**CI only builds these benchmarks.** Nothing runs them, Ghostty publishes no
+result history, and nothing gates regressions. Ghostty does **not** measure:
+
+- input latency
+- frame presentation or dropped frames
+- PTY-to-screen throughput outside of hand runs of `vtebench` or `time cat`
+- memory per line of scrollback
+- startup time
+
+On Windows its IO path is a serial `ReadFile` loop, and no Ghostty GUI exists
+there.
+
+### Why Ghostty is fast (what the builtin engine has to match)
+
+- **SIMD ground state**: Ghostty scans for ESC and decodes UTF-8 to UTF-32 in
+ bulk (simdutf/Highway). Each printable run goes to the screen as one slice
+ (`stream.zig`), not one byte at a time.
+- **Bulk cell writes**: `printSlice` writes same-width runs against a template
+ cell. Widths come from a 3-level lookup table, with a shortcut for
+ codepoints up to 0xFF.
+- **8-byte cells in pooled, offset-addressed pages**: when scrollback is full,
+ the oldest page is reused. Steady-state scrolling allocates nothing.
+- **Separate IO and render threads**: the renderer rebuilds only dirty rows,
+ once per frame, never once per write.
+
+## DT's benchmark layers
+
+All modes live in `tools/Devolutions.Terminal.Bench`. That tool runs on the
+JIT with tiered compilation disabled. It is **not** the NativeAOT shipping
+build, so treat its numbers as relative.
+
+| Layer | Mode | Ghostty counterpart | Comparable on Windows? |
+| --- | --- | --- | --- |
+| 1. VT ingest | `vt` | `ghostty-bench +terminal-stream` | **Yes, in-process on identical bytes** |
+| 2. Frame preparation | `render` (snapshot, plan) | `+screen-clone --mode=render*` | Diagnostic only; the two do different work |
+| 3. CPU paint | `render` (paint-*) | none (GPU renderer) | No |
+| 4. PTY to UI coalescing | `control` | termio/renderer wakeups | No |
+| 5. End-to-end | manual: `vtebench`, `time cat` | same | Linux/macOS only, same machine |
+
+### Layer 1: `vt`, same-process ingest A/B
+
+```powershell
+dotnet run --project tools\Devolutions.Terminal.Bench -c Release -- vt --mb 8 --runs 7 --json artifacts\bench\vt.json
+```
+
+Three targets consume the same bytes in the same chunks (16 KiB by default,
+the ConPTY read size; `--chunk-kb 64` matches `+terminal-stream`):
+
+- `builtin`: DT's `TerminalEngine`, the engine under test.
+- `ghostty-vt`: raw `ghostty_terminal_vt_write` from the pinned
+ `libghostty-vt`, with only the scrollback limit set to the builtin's 9,001
+ lines. This is the bar to beat. It is the same work as `+terminal-stream`,
+ without process startup.
+- `ghostty`: DT's `GhosttyTerminalEngine`, which adds the projection into DT's
+ cell model. This is what the opt-in Ghostty mode actually costs. Projection
+ is lazy, so measure it with `--frame-kb`.
+
+`--frame-kb N` reads one frame every N KiB fed, inside the timed region:
+
+- builtin and `ghostty` take a `CreateSnapshot`;
+- `ghostty-vt` does what Ghostty's renderer does per frame, a render-state
+ update followed by a clean.
+
+The default of `0` measures pure ingest. `256` approximates a busy screen at a
+frame rate the renderer can sustain.
+
+Methodology:
+
+- **Identical, deterministic bytes.** Each workload is seeded, and its
+ SHA-256 is printed. `--export ` writes the `.vt` files so
+ `ghostty-bench` can consume exactly the same input. `--data ` replays
+ a capture, for example an asciinema or vim session.
+- **Equivalence gate.** Before timing, both engines consume the workload, and
+ the visible text, cursor and history are compared. A throughput number only
+ counts when the screens are identical. The `utf8` workload enables DEC mode
+ 2027 because, without it, Ghostty measures ZWJ emoji per codepoint while
+ builtin clusters them. Ghostty trims history by page, so its history count
+ can be a few hundred lines below 9,001; that difference is expected.
+- **Fair timing.**
+ - Engines are built outside the timed region.
+ - Runs interleave the targets in rotating order and include a discarded
+ warmup.
+ - A sample replays the corpus into the same engine until at least `--min-ms`
+ (500 ms by default) has elapsed, so fast targets are not dominated by
+ timer noise.
+ - Laptop CPUs leave turbo a few seconds into sustained load: on the machine
+ below, throughput falls about 3x after ~2.7 s and stays there, whatever
+ the engine does. With 500 ms samples, runs land on both sides of that drop
+ and spreads reach 50–180%. Samples of ~3 s (`--min-ms 3000`, or the
+ script's `-MinMs 3000`) all run at the sustained clock.
+- **Conservative verdict.**
+ - `faster` only when builtin's *slowest* run beats Ghostty's *fastest*.
+ - `slower` when the reverse holds.
+ - `overlap` otherwise.
+ - Without `--no-gate`, the exit code is non-zero unless every workload is
+ `faster`.
+- **Cost metrics**: managed bytes allocated per MiB fed, GCs per MiB, and
+ retained private memory after the run. Retained memory is a process-wide
+ delta, so read it as an order of magnitude. Native allocations by Ghostty
+ appear only in that column.
+
+Workloads (`--workloads a,b,...`):
+
+| Workload | Shape | Source of the idea |
+| --- | --- | --- |
+| `ascii` | unbroken 0x21–0x7E stream relying on autowrap | `ghostty-gen ascii` |
+| `ascii-lines` | printable lines with CRLF | `cat` of a log |
+| `utf8` | Latin, Greek, Cyrillic, CJK, emoji, ZWJ, combining marks | `ghostty-gen utf8`, restricted to assigned characters |
+| `cjk` | lines of CJK ideographs only (wide, 3-byte UTF-8) | `time cat` of CJK text |
+| `emoji` | lines of emoji only (wide, 4-byte UTF-8) | emoji-heavy logs and prompts |
+| `styled` | short SGR runs, OSC 8, wide text | `ghostty-gen styled` |
+| `dense-cells` | every cell gets its own truecolor/256 SGR | vtebench `dense_cells` |
+| `light-cells` | full-screen plain text from home | vtebench `light_cells` |
+| `cursor-motion` | random CUP, short writes, EL | vtebench `cursor_motion`, TUIs |
+| `scrolling` | `y\n` | vtebench `scrolling` |
+| `scroll-region` | DECSTBM region scrolling | vtebench `scrolling_*_region` |
+| `osc` | titles, OSC 133 marks, OSC 8 links | `ghostty-gen osc` (valid sequences only) |
+| `mixed` | the legacy `engine`/`render` corpus | DT history |
+
+### Running the whole suite
+
+```powershell
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 # vt + render, results in artifacts/bench/
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Quick # fast local signal
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Suite vt -MinMs 3000 # ~3 s samples: stable on CPUs that leave turbo under load
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Baseline artifacts/bench//vt.json # fails if builtin/ghostty-vt drops >15% on identical inputs
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -RequireBeatGhostty # fails unless builtin wins every vt workload
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -GhosttyBench \ghostty-bench.exe # also replays the corpora through upstream +terminal-stream
+```
+
+`-GhosttyBench` times whole processes, as Ghostty does, so its figures include
+process startup. It cross-checks the in-process `ghostty-vt` target; it does
+not replace it. Build `ghostty-bench` as described in
+[Ghostty reference measurements](#ghostty-reference-measurements) below.
+
+### End-to-end (layer 5) on Linux/macOS
+
+Ghostty's only cross-terminal numbers come from `time cat` and vtebench. To
+reproduce them, run on one native Linux or macOS machine, with both apps in
+Release and matching font, size, grid and scrollback:
+
+- `time cat` of a large ASCII file and of a large CJK file, 10 runs each.
+- vtebench's 12 workloads, run inside each terminal. vtebench measures how fast
+ the terminal drains the PTY (backpressure), not paint time.
+
+Never run both terminals at the same time; alternate their order and report
+the spread. The checklist in
+[renderer comparison against Ghostty](#renderer-comparison-against-ghostty-linuxmacos)
+lists the full set of gates.
+
+## Results at the time of removal
+
+Setup for these numbers:
+
+- One Windows 11 x64 laptop (Intel Core Ultra 9 285HX, 24 cores, AVX2),
+ .NET 10.0.14 (JIT, tiered compilation off), and the pinned `libghostty-vt`
+ (ReleaseFast).
+- 120x30 grid, 16 KiB chunks, 8 MiB per workload, 7 timed runs plus 1 warmup,
+ samples of at least 3 s.
+- Each workload ran in its own process, via:
+
+ ```powershell
+ pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Suite vt -MinMs 3000
+ ```
+
+This machine's absolute speed drifts by up to 2.5x between invocations,
+mostly with the CPU's turbo state (see *Fair timing* above). So read the
+**builtin / ghostty-vt ratio**, which is measured within each run, and treat
+anything within about ±10% of 1.0 as a tie. Throughput is the median MB/s.
+
+### Ingest: builtin engine vs raw `libghostty-vt`
+
+| Workload | builtin | ghostty-vt | ghostty (DT) | ratio | verdict | builtin alloc / MiB | screens |
+| --- | ---: | ---: | ---: | ---: | --- | ---: | --- |
+| osc | 147 | 25.3 | 23.5 | **5.82x** | **faster** | 6.7 MB | identical |
+| styled | 45.9 | 18.0 | 13.2 | **2.54x** | **faster** | 1.4 MB | 1 row differs |
+| mixed | 787 | 366 | 229 | **2.15x** | **faster** | 8 KB | identical |
+| dense-cells | 262 | 138 | 51 | **1.90x** | **faster** | 0 | identical |
+| scrolling | 56.5 | 32.6 | 31.9 | **1.73x** | **faster** | 104 KB | identical |
+| utf8 | 80.1 | 59.0 | 69.3 | **1.36x** | overlap | 92 KB | identical |
+| light-cells | 2,265 | 1,737 | 62 | **1.30x** | **faster** | 0 | identical |
+| emoji* | 816 | 672 | 301 | **1.21x** | **faster** | 7 KB | identical |
+| ascii | 1,145 | 971 | 448 | **1.18x** | **faster** | 5 KB | identical |
+| ascii-lines | 716 | 626 | 286 | **1.14x** | overlap | 9 KB | identical |
+| cjk* | 583 | 522 | 237 | **1.12x** | **faster** | 10 KB | identical |
+| scroll-region | 865 | 782 | 58 | **1.11x** | **faster** | 0 | identical |
+| cursor-motion | 188 | 172 | 95 | **1.09x** | **faster** | 32 KB | identical |
+
+\* From a later invocation of those two workloads alone. In the full run they
+landed in a noisy stretch (spreads over 50%) and measured 0.98x and 1.22x.
+Other quiet runs of the same code measured 1.07–1.21x on cjk and 1.18x on
+emoji.
+
+`ghostty (DT)` skips projection without `--frame-kb`. The one-row difference in
+`styled` is a behavior difference, not a measurement artifact: builtin drops a
+combining mark that follows a space, and Ghostty keeps it.
+
+How builtin got here, as the median builtin/ghostty-vt ratio per round (each
+round measured against its own in-run Ghostty control):
+
+| Workload | `dc1cd26` | round 1 | round 2 (packed cells) | round 3 | round 4 (`f45b95b`) | round 5 |
+| --- | ---: | ---: | ---: | ---: | ---: | ---: |
+| ascii | 0.009 | 0.20 | 0.55 | 1.21 | 1.24 | 1.18 |
+| ascii-lines | 0.009 | 0.20 | 0.50 | 1.13 | 1.19 | 1.14 |
+| light-cells | 0.009 | 0.32 | 0.48 | 1.20 | 1.23 | 1.30 |
+| scrolling | 0.015 | 0.14 | 0.40 | 1.11 | 1.33 | 1.73 |
+| dense-cells | 0.42 | 0.63 | 0.62 | 1.57 | 1.44 | 1.90 |
+| styled | 0.30 | 1.33 | 1.14 | 2.82 | 2.91 | 2.54 |
+| osc | 0.29 | 2.60 | 4.52 | 4.57 | 4.59 | 5.82 |
+| mixed | 0.03 | 0.39 | 0.45 | 1.56 | 1.84 | 2.15 |
+| scroll-region | 0.018 | 0.22 | 0.34 | 0.90 | 0.96 | 1.11 |
+| cursor-motion | 0.11 | 0.23 | 0.17 | 0.94 | 0.90 | 1.09 |
+| utf8 | 0.10 | 0.36 | 0.27 | 0.57 | 0.77 | 1.36 |
+| cjk | — | — | — | 0.53 | 0.77 | 1.12 |
+| emoji | — | — | — | 0.50 | 0.80 | 1.21 |
+
+Round 5 was measured with 3 s samples; earlier rounds used 500 ms samples.
+
+The `-Baseline` gate flagged real regressions twice:
+
+- **Round 2:** utf8 and cursor-motion.
+- **Round 4:** dense-cells and cursor-motion, from caching packed templates on
+ the short-ASCII path.
+
+Each was confirmed with an interleaved A/B build and fixed before moving on.
+Round 5 passed the gate after each change. Moves of about ±10% between rounds
+(ascii and ascii-lines into round 5, for example) are within this machine's
+noise. styled's 2.54x comes from a run with a 136% spread; single-workload
+runs of the same code measured 3.39–3.61x.
+
+### DT's Ghostty mode vs raw `libghostty-vt`, with frames
+
+These numbers use `--frame-kb 256`, so a projection is taken every 256 KiB.
+They were measured in round 4; round 5 did not change the Ghostty engine.
+
+| Workload | ghostty (DT) before | after | ghostty-vt | ratio before → after |
+| --- | ---: | ---: | ---: | ---: |
+| ascii | 49.8 | 214.4 | 960 | 0.05 → 0.22 |
+| ascii-lines | 46.5 | 220.0 | 660 | 0.07 → 0.33 |
+| cursor-motion | 36.9 | 79.9 | 146 | 0.25 → 0.55 |
+| mixed | 41.1 | 146.5 | 295 | 0.12 → 0.50 |
+| light-cells | 45.6 | 57.5 | 1,580 | 0.03 → 0.04 |
+
+`light-cells` rewrites every row between frames, so every row is dirty and the
+per-cell P/Invoke projection still dominates.
+
+### Frame preparation and CPU paint
+
+`render`, 120x30, median p50 / p95, measured in round 4:
+
+| Stage | before packed cells | now |
+| --- | --- | --- |
+| snapshot | 0.009 / 0.011 ms | 0.040 / 0.044 ms |
+| snapshot + plan | 0.074 / 0.082 ms | 0.113 / 0.129 ms |
+| static paint | 0.69 / 1.08 ms | 0.75 / 1.00 ms |
+| one-row update | 0.69 / 1.70 ms | 0.76 / 1.06 ms |
+| scrolling paint | 3.29 / 9.72 ms | 2.90 / 5.54 ms |
+
+Snapshots now unpack each packed cell into a public `Cell`, which costs about
+0.03 ms per 120x30 frame. Every stage still fits well inside a 16.7 ms frame.
+No Ghostty counterpart exists on Windows.
+
+### What changed in the builtin engine
+
+Every fast path is checked against the per-character path by differential
+tests (`AsciiFastPathTests`, `CsiFastPathTests`, `CellRowModelTests`,
+`PackedCellStorageTests`, `CombiningTableTests`). Each new branch was also
+checked by deliberately breaking it and confirming a test fails.
+
+- **Packed, reference-free storage** (`CellRow`, `PackedCell`).
+ - A cell is 16 bytes with no GC references, down from about 40 with two
+ `string?` fields.
+ - Hyperlinks live in a per-line side array that exists only while needed.
+ - Combining text is interned per buffer (`CombiningTable`), and lines store
+ int IDs. A cluster seen before costs no allocation, a recycled line keeps
+ its ID array, and the table compacts so ever-new clusters cannot grow it
+ without bound. This took utf8 from 9.4 MB to under 0.1 MB allocated per
+ MiB.
+ - The public `Cell` and `TermColor` are unchanged, and every value round-trips
+ losslessly.
+- **Logically blank cells are never written.**
+ - A line stores only its written range; everything outside it reads as the
+ blank it was cleared with.
+ - Recycling a scrollback line, the coldest memory in the buffer, is O(1).
+ - Erasing to the end of a line is O(1) for left-aligned text.
+ - Writes fill only the gap between the stored range and the new cells.
+- **Bulk printing.**
+ - **ASCII:** the parser hands whole printable-ASCII runs to
+ `IVtDispatch.PrintAscii`. They are stored a row segment at a time, two
+ 64-bit (or, with AVX2, two 256-bit per four cells) stores per cell batch.
+ - **UTF-8 runs:** well-formed UTF-8 runs go to `IVtDispatch.PrintRunes`.
+ Runs of 3- or 4-byte sequences (CJK, emoji) are decoded four at a time
+ from 16-byte `Vector128` loads, with the scalar decoder's validity rules
+ (no overlong forms, surrogates or values past U+10FFFF); anything else
+ falls back to the scalar decoder. On CJK this decodes about 3x faster.
+ - **Grapheme joins:** precomputed tables for planes 0 and 1 rule out joins,
+ so runes that cannot join are stored a segment at a time. That includes
+ emoji after a cell without a trailing ZWJ. A zero-width rune with no join
+ property (a combining accent, ZWJ) goes straight into the previous cell's
+ combining text.
+ - **Rows without wide glyphs:** a conservative per-row "may hold wide cells"
+ flag means classifying and storing happen in one pass, with no clearing
+ around wide glyphs. Every freshly recycled scrollback line qualifies. That
+ pass ORs each rune into precomputed template bits and writes one 16-byte
+ store per cell, with widths from a table where ambiguous widths are already
+ resolved.
+ - **Short writes:** the packed print templates are cached and rebuilt only
+ when the attributes, protection or shell-integration state change, and a
+ single printable byte between escape sequences is one store.
+- **CSI fast path.** A complete plain CSI sequence is parsed in one pass,
+ straight into the arrays the dispatcher receives, and the parser calls the
+ engine directly instead of through `IVtDispatch`. Sequences with no private
+ marker or intermediate byte skip the special cases and go straight to the
+ switch on the final byte.
+- **C0 fast path.** CR, LF and other controls in ground state go straight to
+ `ExecuteC0` instead of through the string-state checks.
+- **Scroll regions** rotate line references with `Array.Copy`, not one
+ indexer call per line.
+
+### What still separates builtin from Ghostty
+
+Builtin now leads on every workload. The closest leads, `cursor-motion`,
+`scroll-region` and `cjk` (1.09–1.12x), sit near the ±10% band of this
+machine's noise, and `ascii-lines` and `utf8` still get `overlap` verdicts
+from run-to-run spread. Timed stage by stage on CJK after round 5 (one engine
+reused with history full, per scalar, all stages in one process):
+
+| Stage | Cost |
+| --- | --- |
+| UTF-8 decode, block path | ~0.5–1.0 ns |
+| Parser per-line work (CR/LF dispatch, run setup) | ~0.7–1 ns |
+| Cell stores (two 16-byte cells per wide character) | ~2 ns |
+| Buffer CR/LF, scrolling and line reuse | ~0.7 ns |
+
+- **Parser per-line overhead** now costs about as much as the decode. Each
+ CR and LF still goes through an interface call, and each line's run of
+ characters pays its setup once.
+- **Cell size.** Packed cells are 16 bytes against Ghostty's 8, so storing,
+ clearing or copying a row moves twice the memory. Getting to 8 bytes means
+ interning styles, which is a larger change.
+- **What did not help.** Prefetching the scrollback lines about to be reused
+ measured slower: with history full, steady-state throughput is the same at
+ 100 and 9,000 lines, so cold recycled lines are not a bottleneck.
+
+**Variance.** Within isolated processes the ratio repeats to about ±10% on
+this machine with 3 s samples. With 500 ms samples, runs straddle the CPU's
+turbo drop and single workloads swing by 50% or more. In one long process
+running every workload, ratios also drifted by up to 50%, because heap state
+carried over between workloads. For that reason the script runs one process
+per workload, and `-Baseline` only compares workloads with the same SHA-256
+and gates on the ratio, not on absolute MB/s.
+
+## Definition of "matches or beats Ghostty"
+
+A claim needs every row below. A missing or failed row means the claim has
+not been established.
+
+1. `vt`: `faster` (or, for "matches", `overlap` with a median ratio ≥ 0.95)
+ on every workload, with identical screens, on at least one x64 and one
+ arm64 host.
+2. The same corpora through upstream `ghostty-bench +terminal-stream` do not
+ contradict layer 1. This rules out a P/Invoke artifact.
+3. Scrolling allocation is bounded. Steady-state `scrolling` should allocate
+ close to 0 B/MiB once history is full, as Ghostty's page reuse does.
+4. On Linux/macOS, `time cat` and vtebench place DT at or above Ghostty, run
+ under the conditions described in the end-to-end section.
+5. Frame latency and dropped frames: measure these even though Ghostty does
+ not publish them. Input latency (key to PTY echo to presented frame) is
+ unmeasured on both sides, and a DT measurement would be new evidence rather
+ than a comparison.
+
+Run `scripts/Invoke-TerminalBenchmarks.ps1 -Baseline` on every engine change
+so wins are kept, and record the commit, machine and `vt.json` with each
+published number.
+
+## Ghostty reference measurements
+
+These notes were moved here from
+[renderer benchmarks](../renderer-benchmarks.md) when the Ghostty engine was
+removed.
+
+This repository pinned Ghostty at the commit above (then recorded in
+`native/ghostty/ghostty-upstream.json`). Build **that** source with Zig 0.16.0
+using `zig build -Demit-bench -Doptimize=ReleaseFast` (on macOS add
+`-Demit-macos-app=false`). With the pinned revision on Windows, build the
+benchmark-only/lib-vt targets instead; the default full target failed to
+link because of a duplicate `memset` symbol:
+
+```powershell
+zig build -Demit-bench=true -Demit-lib-vt=true -Doptimize=ReleaseFast -j1
+```
+
+The upstream `zig-out/bin/ghostty-bench` (`.exe` on Windows) can consume the
+exported corpus:
+
+```text
+ghostty-bench +screen-clone --mode=render-partial --terminal-rows=30 --terminal-cols=120 --data=
+```
+
+Run it repeatedly with warmups (e.g. `hyperfine`) rather than timing
+`ghostty-gen | ghostty-bench`: keep generation out of the measurement and use
+enough internal operations to amortize process startup and input loading.
+Ghostty's `screen-clone` `render`, `render-clean`, and
+`render-partial` modes time updates to its CPU `RenderState`, **not** GPU
+painting. Their steps respectively perform 50,000 full, 3,000,000 clean, or
+2,000,000 partial updates at `--loops=1`. External wall times also include
+process startup and corpus loading. They use different semantics and
+iteration counts from this project's snapshot/plan stage; record them as
+separate diagnostics, not a numeric speed ratio. The `libghostty-vt` binary
+used by this project had no Ghostty graphics renderer.
+
+## Renderer comparison against Ghostty (Linux/macOS)
+
+Use the general procedure in
+[renderer benchmarks](../renderer-benchmarks.md#full-renderer-comparison-linuxmacos).
+For Ghostty specifically, instrument upstream Ghostty at `RenderState.update`
+and `renderer/generic.zig:drawFrame`. Ghostty's native GUI is available on
+Linux/macOS, not Windows; its Windows `libghostty-vt` support cannot stand in
+for a native renderer comparison.
+
+For a claim of beating Ghostty **at every measured point**, establish separate
+equivalent baselines for VT ingest, CPU frame preparation, idle/static paint,
+localized updates, first-pass and replayed scrolling, and presented frame
+latency. Compare p50, p95, p99, allocation/peak resident memory, CPU/GPU
+utilization, and dropped frames for each workload on the same native host;
+require pixel or terminal-state equivalence first. Alternate the applications
+across repeated runs and report the run-to-run spread as well as medians. A
+failure or unmeasured metric means the all-points claim has **not** passed.
+The Windows offscreen raster and upstream `screen-clone` results measure
+different work and cannot satisfy the presented-frame gate.
diff --git a/docs/iseberg.md b/docs/iseberg.md
index 08ffdf0..3ac05ca 100644
--- a/docs/iseberg.md
+++ b/docs/iseberg.md
@@ -296,7 +296,8 @@ Iseberg CI runs test projects sequentially (`-m:1`) to avoid concurrent test-hos
startup starving short broker deadlines. It retains TRX results and per-test
hang diagnostics and a 20-minute test-step budget for the slower hosted Windows
runner. Native libraries are restored in a preceding step, so a native-cache miss
-(Zig download and Ghostty build) does not consume that budget. A failed App test
+(on Linux and macOS, the Zig download and `dt-pty-host` build) does not
+consume that budget. A failed App test
project stops the step before starting the UI suite.
Run the same core locally after installing the test runtime:
diff --git a/docs/macos.md b/docs/macos.md
index d32b959..a48986b 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -1,7 +1,7 @@
# macOS support
macOS is a first-class host for the managed app, Unix PTY transport, built-in
-and Ghostty engines, and self-contained `.app` packaging (including signed,
+engine, and self-contained `.app` packaging (including signed,
notarized `.dmg` release artifacts). Global hotkeys, default terminal
registration, and Homebrew remain out of scope.
@@ -9,7 +9,7 @@ registration, and Homebrew remain out of scope.
- Avalonia desktop host (`osx-arm64` / `osx-x64`)
- Local shells through `dt-pty-host` (`forkpty`, same framing as Linux)
-- Selectable built-in and Ghostty engines (`libghostty-vt.dylib`)
+- Built-in terminal engine
- Settings at `~/Library/Application Support/Devolutions/Terminal/`
- Native Command-key defaults for clipboard, tabs, windows, search, settings,
selection, font sizing, full screen, and quit actions
@@ -39,7 +39,7 @@ registration, and Homebrew remain out of scope.
- Global hotkeys (broker / `dt -w` still work)
- Default-terminal registration
-Ghostty dylibs and `dt-pty-host` are built on restore for `osx-arm64` /
+`dt-pty-host` is built on restore for `osx-arm64` /
`osx-x64` (macOS 13+). `dt-pty-host` is compiled with Apple clang so it can
link `libutil` from the SDK.
diff --git a/docs/parity-status.md b/docs/parity-status.md
index 7bc4015..d941150 100644
--- a/docs/parity-status.md
+++ b/docs/parity-status.md
@@ -2,7 +2,7 @@
This document is the authoritative measured parity record for the .NET 10,
NativeAOT, and Avalonia port. [history/porting.md](history/porting.md) records the historical roadmap;
-this file records measured behavior after the Windows, Ghostty, and Linux
+this file records measured behavior after the Windows and Linux
milestones.
Implementation, unit, headless Avalonia, protocol, process, package-structure,
@@ -15,7 +15,7 @@ and NativeAOT gates completed before the final live UI matrix.
| Windows process transport | ConPTY with input, resize, cancellation, restart, exit metadata, and x64/ARM64 NativeAOT |
| Linux process transport | Bundled `forkpty` relay with input, resize, cancellation, restart, exit metadata, and x64/ARM64 NativeAOT |
| macOS process transport | Same Unix `forkpty` host (`osx-arm64`/`osx-x64`); Apple clang `dt-pty-host` plus NativeAOT `.app` packaging on Darwin |
-| Terminal engines | Selectable built-in and pinned Ghostty engines on Windows, Linux, and macOS |
+| Terminal engine | Built-in engine (`Devolutions.Terminal.Core.TerminalEngine`) on Windows, Linux, and macOS |
| Application shell | Multi-window broker, tabs, panes, settings editor, palettes, tray behavior, accessibility, clipboard, and notifications |
| Settings | Layering, dynamic profiles, fragments, source-generated JSON, Windows paths, XDG paths, macOS Application Support, and state persistence |
| Distribution | Windows x64/ARM64 MSIX and bundle; reproducible Linux x64/ARM64 tar, DEB, RPM, and AppImage packages with canonical freedesktop assets, licenses, checksums, inventory/SPDX SBOM, and DESTDIR-aware helpers; macOS NativeAOT `.app` and zip on Darwin, plus signed, notarized `.dmg` release artifacts from CI |
@@ -101,17 +101,17 @@ process environment block.
| Area | Status | Remaining acceptance |
| --- | --- | --- |
-| Search | Implemented across both engines; Core canonicalizes equivalent Unicode spellings and maps ZWJ/wide grapheme matches to cell ranges; built-in search anchors the selected match by logical-line ID and cell offset across reflow, duplicate lines, viewport scrolling, and bounded scrollback eviction, selecting the next surviving match if the selected line is lost | Ghostty projection regenerates line IDs, so selection uses a text/neighbor heuristic; its pinned render ABI does not expose equivalent cluster geometry |
-| Grapheme/emoji | Built-in supports Hangul, Indic conjuncts, prepend, spacing marks, RI pairs, emoji modifiers/selectors, and emoji ZWJ sequences at arbitrary UTF-8 feed boundaries; bundled Noto Color Emoji provides deterministic Linux fallback | Unicode 16.0.0 `GraphemeBreakTest.txt` is pinned: all 369 terminal-representable rows pass ordered-cell and chunk-invariance checks. The other 724 rows are classified as unsuitable for terminal-cell assertions, not claimed as pure-segmenter passes; full UAX #29 conformance outside the tested subset and equivalent Ghostty cluster geometry remain unavailable |
-| Row rendition | Built-in DECDWL/DECDHL parser, snapshots, logical cursor clipping, reflow preservation, and render transforms implemented | The pinned Ghostty C ABI does not expose row rendition; capability is explicitly unavailable there |
-| Sixel | Built-in decode/render, DECSDM scrolling/display behavior, retained cell geometry, and stable ownership implemented | The pinned Ghostty C ABI exposes no image resources and reports the capability unavailable |
-| OSC 1337 | Built-in bounded inline decode/render and stable ownership implemented; non-inline transfer is explicitly rejected without I/O | The pinned Ghostty C ABI exposes no image resources and reports the capability unavailable |
-| ConEmu images | Bounded single-part `st=0;sz=` payloads decode into shared overlay metadata and render safely | Multipart payloads are explicitly rejected; the pinned Ghostty C ABI exposes no image resources |
-| Kitty graphics | Direct-medium RGBA/RGB/PNG transmissions with chunking, zlib, placements, crop, z-order, deletes, and query responses implemented; unsupported media and animation are rejected deterministically | File/shared-memory media and animation frames are intentional gaps; the pinned Ghostty C ABI exposes no image resources and reports the capability unavailable |
-| Image ownership | Stable logical-line anchors survive scrollback and reflow in main and alternate buffers and are removed on owning-line eviction | Ghostty image projection is unavailable in the pinned C ABI and produces deterministic unsupported diagnostics |
-| VT52 | Output plus host cursor/PF/application-keypad encoding implemented, with built-in/Ghostty differential mode coverage | No remaining shared subset work |
-| DRCS | Built-in parse/resource mapping, bounded snapshot masks, render planning, and downloaded-pixel rendering implemented | The pinned Ghostty C ABI does not expose DRCS resources; capability is explicitly unavailable there |
-| Extended keyboard | Built-in Kitty set/query/push/pop flags, CSI-u event bytes, `modifyOtherKeys`, Win32-input mode, and press/repeat/release encoding implemented | Kitty alternate-key reporting and associated-text reporting are not advertised; the pinned Ghostty C ABI exposes no keyboard protocol state and reports these capabilities unavailable |
+| Search | Implemented in the built-in engine; Core canonicalizes equivalent Unicode spellings and maps ZWJ/wide grapheme matches to cell ranges; built-in search anchors the selected match by logical-line ID and cell offset across reflow, duplicate lines, viewport scrolling, and bounded scrollback eviction, selecting the next surviving match if the selected line is lost | None |
+| Grapheme/emoji | Built-in supports Hangul, Indic conjuncts, prepend, spacing marks, RI pairs, emoji modifiers/selectors, and emoji ZWJ sequences at arbitrary UTF-8 feed boundaries; bundled Noto Color Emoji provides deterministic Linux fallback | Unicode 16.0.0 `GraphemeBreakTest.txt` is pinned: all 369 terminal-representable rows pass ordered-cell and chunk-invariance checks. The other 724 rows are classified as unsuitable for terminal-cell assertions, not claimed as pure-segmenter passes; full UAX #29 conformance outside the tested subset remains unavailable |
+| Row rendition | Built-in DECDWL/DECDHL parser, snapshots, logical cursor clipping, reflow preservation, and render transforms implemented | None |
+| Sixel | Built-in decode/render, DECSDM scrolling/display behavior, retained cell geometry, and stable ownership implemented | None |
+| OSC 1337 | Built-in bounded inline decode/render and stable ownership implemented; non-inline transfer is explicitly rejected without I/O | None |
+| ConEmu images | Bounded single-part `st=0;sz=` payloads decode into shared overlay metadata and render safely | Multipart payloads are explicitly rejected |
+| Kitty graphics | Direct-medium RGBA/RGB/PNG transmissions with chunking, zlib, placements, crop, z-order, deletes, and query responses implemented; unsupported media and animation are rejected deterministically | File/shared-memory media and animation frames are intentional gaps |
+| Image ownership | Stable logical-line anchors survive scrollback and reflow in main and alternate buffers and are removed on owning-line eviction | None |
+| VT52 | Output plus host cursor/PF/application-keypad encoding implemented | None |
+| DRCS | Built-in parse/resource mapping, bounded snapshot masks, render planning, and downloaded-pixel rendering implemented | None |
+| Extended keyboard | Built-in Kitty set/query/push/pop flags, CSI-u event bytes, `modifyOtherKeys`, Win32-input mode, and press/repeat/release encoding implemented | Kitty alternate-key reporting and associated-text reporting are not advertised |
| Shader effects | Optional deterministic, bounded Skia retro/scanline pass, toggleable per active terminal | Custom arbitrary HLSL/pixel-shader files are not loaded or advertised |
The image regression matrix covers split protocol feeds, exact/over-limit
@@ -146,7 +146,7 @@ The `linux-arm64-hardware` CI job runs on GitHub's native
not `aarch64` or `arm64`; emulation is not an accepted substitute. The job
downloads the cross-built ARM64 tar, DEB, RPM, and AppImage artifacts and runs
`scripts/Test-LinuxArm64Runtime.sh`. The gate executes the NativeAOT `wt`
-help/error paths from every extracted format, Ghostty ABI and engine feeds, the
+help/error paths from every extracted format, the
built-in parser/engine, real `forkpty` input/resize/exit/cancellation/restart,
broker concurrency, Linux shell profile and XDG paths, and disposable-root
package install/upgrade/uninstall.
@@ -161,8 +161,8 @@ install/upgrade/uninstall verification for Windows x64/ARM64 packages.
## Final UI results
-Windows x64 and packaged Linux x64 were validated with built-in and Ghostty
-engines after the full non-UI matrix. The runs covered three independent tabs,
+Windows x64 and packaged Linux x64 were validated with the built-in
+engine after the full non-UI matrix. The runs covered three independent tabs,
split panes, focus/resize/close isolation, clipboard, fullscreen,
maximize/restore, settings, workspace recreation, marks, summon/quake,
truecolor, Unicode/CJK/emoji, row rendition, images, and shader effects.
@@ -170,9 +170,7 @@ truecolor, Unicode/CJK/emoji, row rendition, images, and shader effects.
- PowerShell 7 loaded PSReadLine normally. Windows PowerShell 5.1 on the test
machine rejected its PSReadLine script module because of the machine's
execution policy; ConPTY handles were confirmed as non-redirected.
-- Built-in Linux rendered Sixel and inline OSC 1337 images. Ghostty displayed
- explicit limitation notifications for image protocols and row rendition that
- its pinned C ABI does not expose.
+- Built-in Linux rendered Sixel and inline OSC 1337 images.
- Bundled Noto Color Emoji fixed Linux emoji-ZWJ tofu. Portable settings icons
and narrower content padding removed Linux settings-page clipping.
- Mark actions display notifications and colored scrollbar ticks.
@@ -180,7 +178,7 @@ truecolor, Unicode/CJK/emoji, row rendition, images, and shader effects.
provide a visible file-manager handler.
- Native Windows ARM64 and Linux ARM64 remain non-UI hardware/package gates;
no ARM64 desktop was available in this session.
-- macOS ARM64 NativeAOT `.app` packaging, `dt` parser, Ghostty ABI, real
+- macOS ARM64 NativeAOT `.app` packaging, `dt` parser, real
`forkpty`, broker named-pipe length, and a live GUI-host process start were
validated on Darwin. Signed, notarized `.dmg` release artifacts are produced
in CI by `scripts/Release-MacOsPackage.ps1` (see [macos.md](macos.md)).
diff --git a/docs/performance-benchmarks.md b/docs/performance-benchmarks.md
index 421aac8..4cb5222 100644
--- a/docs/performance-benchmarks.md
+++ b/docs/performance-benchmarks.md
@@ -1,79 +1,13 @@
-# Performance benchmarks vs Ghostty
+# Performance benchmarks
-This page explains how Ghostty backs up its "fastest terminal" positioning,
-what its own benchmarks do and do not measure, and how DT benchmarks its
-**builtin** engine and renderer against it. The goal is a claim that holds up
-under review: "with the default engine, DT matches or beats Ghostty at
-workload X, measured this way, on this machine."
+This page explains how DT benchmarks VT ingest in its terminal engine
+(`Devolutions.Terminal.Core.TerminalEngine`) and how to read the results
+across sessions and machines.
For renderer-only details (caches, pixel verification, historical numbers)
-see [renderer benchmarks](renderer-benchmarks.md).
-
-## How Ghostty supports its performance claims
-
-Ghostty's public claims are mostly qualitative. Few of them come with a method
-someone else could rerun.
-
-| Claim | Where | Evidence |
-| --- | --- | --- |
-| "Same performance category" as the fastest terminals; "~100x faster than Terminal.app and iTerm" | README | None. Ghostty's own devlog measured about 6x against iTerm2 and under 2x against Terminal.app |
-| `cat` of plain text 4x faster than iTerm/Kitty, ~60 fps under load | v1.0 README | No method given. The README itself says "we need better benchmarks" |
-| SIMD parser: 7.3x faster ASCII parse, 16.6x faster UTF-8 decode; `time cat` of a 5.4 MB Japanese text: Ghostty 73 ms, Alacritty 66 ms, Kitty 392 ms, iTerm2 470 ms | [Devlog 006](https://mitchellh.com/writing/ghostty-devlog-006) | **The only write-up with a real method**: hyperfine microbenchmarks with noop/memcpy floors, plus 10-run `time cat` averages. Alacritty beat Ghostty in that table |
-| 480–500 fps in DOOM-fire | [Zig patterns talk](https://mitchellh.com/writing/ghostty-and-useful-zig-patterns) | Anecdote. The same talk says input latency was never measured |
-| 1.3: IO 20–30% faster on a 4 GB asciinema replay; frame builds 2–5x faster | [1.3.0 notes](https://ghostty.org/docs/install/release-notes/1-3-0) | Real-world replay, but the corpus is not public |
-| `cat` of 150 MB ASCII in 575 ms (nightly) | Social post, second-hand | One run, no hardware listed. Unverified |
-
-The [about page](https://ghostty.org/docs/about) itself is careful: Ghostty is
-"in some benchmarks faster, in others slower."
-
-### What Ghostty's in-tree benchmarks measure
-
-Ghostty's benchmarks live in `src/benchmark/` at the pinned commit (see
-`native/ghostty/ghostty-upstream.json`). They are built with
-`zig build -Demit-bench`, which produces two binaries:
-
-- `ghostty-bench +` runs one component microbenchmark. It prints
- nothing; the documented method is to time the **whole process** with
- `hyperfine --warmup N`. That time includes process startup and setup. This is
- why many actions have a `noop` mode to subtract.
-- `ghostty-gen ` writes synthetic input: `ascii`, `utf8`, `osc`,
- `kitty` and `styled`. Seeds are time-based by default, so you save the output
- once and reuse the file.
-
-The actions that matter for a comparison with DT:
-
-| Action | What it times | Closest DT measurement |
-| --- | --- | --- |
-| `+terminal-stream --data=F` | Full VT parse and terminal state update, read in 64 KiB chunks | `vt` (`ghostty-vt` target) |
-| `+terminal-parser` | State machine only, actions discarded | none (DT's parser is not separable) |
-| `+screen-clone --mode=render\|render-clean\|render-partial` | Updates to the CPU `RenderState` while holding the lock | `render` snapshot/plan |
-| `+codepoint-width`, `+grapheme-break` | Unicode tables | none |
-| `+terminal-resize` | Reflow | none yet |
-
-**CI only builds these benchmarks.** Nothing runs them, Ghostty publishes no
-result history, and nothing gates regressions. Ghostty does **not** measure:
-
-- input latency
-- frame presentation or dropped frames
-- PTY-to-screen throughput outside of hand runs of `vtebench` or `time cat`
-- memory per line of scrollback
-- startup time
-
-On Windows its IO path is a serial `ReadFile` loop, and no Ghostty GUI exists
-there.
-
-### Why Ghostty is fast (what the builtin engine has to match)
-
-- **SIMD ground state**: Ghostty scans for ESC and decodes UTF-8 to UTF-32 in
- bulk (simdutf/Highway). Each printable run goes to the screen as one slice
- (`stream.zig`), not one byte at a time.
-- **Bulk cell writes**: `printSlice` writes same-width runs against a template
- cell. Widths come from a 3-level lookup table, with a shortcut for
- codepoints up to 0xFF.
-- **8-byte cells in pooled, offset-addressed pages**: when scrollback is full,
- the oldest page is reused. Steady-state scrolling allocates nothing.
-- **Separate IO and render threads**: the renderer rebuilds only dirty rows,
- once per frame, never once per write.
+see [renderer benchmarks](renderer-benchmarks.md). The earlier comparison
+against `libghostty-vt`, from before the Ghostty engine was removed, is kept in
+[Ghostty comparison benchmarks](history/ghostty-comparison-benchmarks.md).
## DT's benchmark layers
@@ -81,345 +15,91 @@ All modes live in `tools/Devolutions.Terminal.Bench`. That tool runs on the
JIT with tiered compilation disabled. It is **not** the NativeAOT shipping
build, so treat its numbers as relative.
-| Layer | Mode | Ghostty counterpart | Comparable on Windows? |
-| --- | --- | --- | --- |
-| 1. VT ingest | `vt` | `ghostty-bench +terminal-stream` | **Yes, in-process on identical bytes** |
-| 2. Frame preparation | `render` (snapshot, plan) | `+screen-clone --mode=render*` | Diagnostic only; the two do different work |
-| 3. CPU paint | `render` (paint-*) | none (GPU renderer) | No |
-| 4. PTY to UI coalescing | `control` | termio/renderer wakeups | No |
-| 5. End-to-end | manual: `vtebench`, `time cat` | same | Linux/macOS only, same machine |
+| Layer | Mode |
+| --- | --- |
+| 1. VT ingest | `vt` |
+| 2. Frame preparation | `render` (snapshot, plan) |
+| 3. CPU paint | `render` (paint-*) |
+| 4. PTY to UI coalescing | `control` |
-### Layer 1: `vt`, same-process ingest A/B
+## `vt`: same-process ingest
```powershell
dotnet run --project tools\Devolutions.Terminal.Bench -c Release -- vt --mb 8 --runs 7 --json artifacts\bench\vt.json
```
-Three targets consume the same bytes in the same chunks (16 KiB by default,
-the ConPTY read size; `--chunk-kb 64` matches `+terminal-stream`):
+Two targets consume the same bytes in the same chunks (16 KiB by default,
+the ConPTY read size; change it with `--chunk-kb`):
- `builtin`: DT's `TerminalEngine`, the engine under test.
-- `ghostty-vt`: raw `ghostty_terminal_vt_write` from the pinned
- `libghostty-vt`, with only the scrollback limit set to the builtin's 9,001
- lines. This is the bar to beat. It is the same work as `+terminal-stream`,
- without process startup.
-- `ghostty`: DT's `GhosttyTerminalEngine`, which adds the projection into DT's
- cell model. This is what the opt-in Ghostty mode actually costs. Projection
- is lazy, so measure it with `--frame-kb`.
+- `control`: an FNV-1a hash over the fed bytes. Each step depends on the
+ previous one, so it is a serial loop that does no terminal work, and its
+ speed tracks only the core clock and load.
-`--frame-kb N` reads one frame every N KiB fed, inside the timed region:
+The control exists because absolute MB/s is not stable. On laptop CPUs it
+drifts by up to about 2.5x between invocations, mostly with the CPU's turbo
+state. Both targets see the same clock within a run, so the
+**builtin / control ratio** (`builtinVsControl` in the JSON) is the number to
+compare across sessions. Absolute MB/s is only meaningful within one run.
-- builtin and `ghostty` take a `CreateSnapshot`;
-- `ghostty-vt` does what Ghostty's renderer does per frame, a render-state
- update followed by a clean.
-
-The default of `0` measures pure ingest. `256` approximates a busy screen at a
-frame rate the renderer can sustain.
+`--frame-kb N` makes `builtin` take a `CreateSnapshot` every N KiB fed,
+inside the timed region. The default of `0` measures pure ingest. `256`
+approximates a busy screen at a frame rate the renderer can sustain.
Methodology:
- **Identical, deterministic bytes.** Each workload is seeded, and its
- SHA-256 is printed. `--export ` writes the `.vt` files so
- `ghostty-bench` can consume exactly the same input. `--data ` replays
- a capture, for example an asciinema or vim session.
-- **Equivalence gate.** Before timing, both engines consume the workload, and
- the visible text, cursor and history are compared. A throughput number only
- counts when the screens are identical. The `utf8` workload enables DEC mode
- 2027 because, without it, Ghostty measures ZWJ emoji per codepoint while
- builtin clusters them. Ghostty trims history by page, so its history count
- can be a few hundred lines below 9,001; that difference is expected.
+ SHA-256 is printed. `--export ` writes the `.vt` files. `--data `
+ replays a capture, for example an asciinema or vim session.
- **Fair timing.**
- Engines are built outside the timed region.
- Runs interleave the targets in rotating order and include a discarded
- warmup.
+ warmup, so thermal drift and background load hit both alike.
- A sample replays the corpus into the same engine until at least `--min-ms`
- (500 ms by default) has elapsed, so fast targets are not dominated by
+ (500 ms by default) has elapsed, so fast workloads are not dominated by
timer noise.
- - Laptop CPUs leave turbo a few seconds into sustained load: on the machine
- below, throughput falls about 3x after ~2.7 s and stays there, whatever
- the engine does. With 500 ms samples, runs land on both sides of that drop
- and spreads reach 50–180%. Samples of ~3 s (`--min-ms 3000`, or the
- script's `-MinMs 3000`) all run at the sustained clock.
-- **Conservative verdict.**
- - `faster` only when builtin's *slowest* run beats Ghostty's *fastest*.
- - `slower` when the reverse holds.
- - `overlap` otherwise.
- - Without `--no-gate`, the exit code is non-zero unless every workload is
- `faster`.
+ - Laptop CPUs leave turbo a few seconds into sustained load, and throughput
+ can fall about 3x and stay there. With 500 ms samples, runs land on both
+ sides of that drop and spreads become large. Samples of ~3 s
+ (`--min-ms 3000`, or the script's `-MinMs 3000`) all run at the sustained
+ clock.
- **Cost metrics**: managed bytes allocated per MiB fed, GCs per MiB, and
retained private memory after the run. Retained memory is a process-wide
- delta, so read it as an order of magnitude. Native allocations by Ghostty
- appear only in that column.
-
-Workloads (`--workloads a,b,...`):
+ delta, so read it as an order of magnitude.
-| Workload | Shape | Source of the idea |
-| --- | --- | --- |
-| `ascii` | unbroken 0x21–0x7E stream relying on autowrap | `ghostty-gen ascii` |
-| `ascii-lines` | printable lines with CRLF | `cat` of a log |
-| `utf8` | Latin, Greek, Cyrillic, CJK, emoji, ZWJ, combining marks | `ghostty-gen utf8`, restricted to assigned characters |
-| `cjk` | lines of CJK ideographs only (wide, 3-byte UTF-8) | `time cat` of CJK text |
-| `emoji` | lines of emoji only (wide, 4-byte UTF-8) | emoji-heavy logs and prompts |
-| `styled` | short SGR runs, OSC 8, wide text | `ghostty-gen styled` |
-| `dense-cells` | every cell gets its own truecolor/256 SGR | vtebench `dense_cells` |
-| `light-cells` | full-screen plain text from home | vtebench `light_cells` |
-| `cursor-motion` | random CUP, short writes, EL | vtebench `cursor_motion`, TUIs |
-| `scrolling` | `y\n` | vtebench `scrolling` |
-| `scroll-region` | DECSTBM region scrolling | vtebench `scrolling_*_region` |
-| `osc` | titles, OSC 133 marks, OSC 8 links | `ghostty-gen osc` (valid sequences only) |
-| `mixed` | the legacy `engine`/`render` corpus | DT history |
+Workloads (`--workloads a,b,...`; `vt --list` prints them):
-### Running the whole suite
+| Workload | Shape |
+| --- | --- |
+| `ascii` | unbroken 0x21–0x7E stream relying on autowrap |
+| `ascii-lines` | printable lines with CRLF (`cat` of a log) |
+| `utf8` | Latin, Greek, Cyrillic, CJK, emoji, ZWJ, combining marks (DEC mode 2027 on) |
+| `cjk` | lines of CJK ideographs only (wide, 3-byte UTF-8) |
+| `emoji` | lines of emoji only (wide, 4-byte UTF-8) |
+| `styled` | short SGR runs, OSC 8, wide text |
+| `dense-cells` | every cell gets its own truecolor/256 SGR (vtebench `dense_cells`) |
+| `light-cells` | full-screen plain text from home (vtebench `light_cells`) |
+| `cursor-motion` | random CUP, short writes, EL (vtebench `cursor_motion`, TUIs) |
+| `scrolling` | `y\n` (vtebench `scrolling`) |
+| `scroll-region` | DECSTBM region scrolling (vtebench `scrolling_*_region`) |
+| `osc` | titles, OSC 133 marks, OSC 8 links |
+| `mixed` | the legacy `engine`/`render` corpus |
+
+## Running the whole suite
```powershell
-pwsh scripts/Invoke-TerminalBenchmarks.ps1 # vt + render, results in artifacts/bench/
-pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Quick # fast local signal
-pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Suite vt -MinMs 3000 # ~3 s samples: stable on CPUs that leave turbo under load
-pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Baseline artifacts/bench//vt.json # fails if builtin/ghostty-vt drops >15% on identical inputs
-pwsh scripts/Invoke-TerminalBenchmarks.ps1 -RequireBeatGhostty # fails unless builtin wins every vt workload
-pwsh scripts/Invoke-TerminalBenchmarks.ps1 -GhosttyBench \ghostty-bench.exe # also replays the corpora through upstream +terminal-stream
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 # vt + render, results in artifacts/bench/
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Quick # fast local signal
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Suite vt -MinMs 3000 # ~3 s samples: stable on CPUs that leave turbo under load
+pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Baseline artifacts/bench//vt.json # fails if builtin/control drops >15% on identical inputs
```
-`-GhosttyBench` times whole processes, as Ghostty does, so its figures include
-process startup. It cross-checks the in-process `ghostty-vt` target; it does
-not replace it. Build `ghostty-bench` as described in
-[renderer benchmarks](renderer-benchmarks.md#ghostty-reference-measurements).
-
-### End-to-end (layer 5) on Linux/macOS
-
-Ghostty's only cross-terminal numbers come from `time cat` and vtebench. To
-reproduce them, run on one native Linux or macOS machine, with both apps in
-Release and matching font, size, grid and scrollback:
-
-- `time cat` of a large ASCII file and of a large CJK file, 10 runs each.
-- vtebench's 12 workloads, run inside each terminal. vtebench measures how fast
- the terminal drains the PTY (backpressure), not paint time.
-
-Never run both terminals at the same time; alternate their order and report
-the spread. The checklist in
-[renderer benchmarks](renderer-benchmarks.md#full-renderer-comparison-linuxmacos)
-lists the full set of gates.
-
-## Current results
-
-Setup for these numbers:
-
-- One Windows 11 x64 laptop (Intel Core Ultra 9 285HX, 24 cores, AVX2),
- .NET 10.0.14 (JIT, tiered compilation off), and the pinned `libghostty-vt`
- (ReleaseFast).
-- 120x30 grid, 16 KiB chunks, 8 MiB per workload, 7 timed runs plus 1 warmup,
- samples of at least 3 s.
-- Each workload ran in its own process, via:
-
- ```powershell
- pwsh scripts/Invoke-TerminalBenchmarks.ps1 -Suite vt -MinMs 3000
- ```
-
-This machine's absolute speed drifts by up to 2.5x between invocations,
-mostly with the CPU's turbo state (see *Fair timing* above). So read the
-**builtin / ghostty-vt ratio**, which is measured within each run, and treat
-anything within about ±10% of 1.0 as a tie. Throughput is the median MB/s.
-
-### Ingest: builtin engine vs raw `libghostty-vt`
-
-| Workload | builtin | ghostty-vt | ghostty (DT) | ratio | verdict | builtin alloc / MiB | screens |
-| --- | ---: | ---: | ---: | ---: | --- | ---: | --- |
-| osc | 147 | 25.3 | 23.5 | **5.82x** | **faster** | 6.7 MB | identical |
-| styled | 45.9 | 18.0 | 13.2 | **2.54x** | **faster** | 1.4 MB | 1 row differs |
-| mixed | 787 | 366 | 229 | **2.15x** | **faster** | 8 KB | identical |
-| dense-cells | 262 | 138 | 51 | **1.90x** | **faster** | 0 | identical |
-| scrolling | 56.5 | 32.6 | 31.9 | **1.73x** | **faster** | 104 KB | identical |
-| utf8 | 80.1 | 59.0 | 69.3 | **1.36x** | overlap | 92 KB | identical |
-| light-cells | 2,265 | 1,737 | 62 | **1.30x** | **faster** | 0 | identical |
-| emoji* | 816 | 672 | 301 | **1.21x** | **faster** | 7 KB | identical |
-| ascii | 1,145 | 971 | 448 | **1.18x** | **faster** | 5 KB | identical |
-| ascii-lines | 716 | 626 | 286 | **1.14x** | overlap | 9 KB | identical |
-| cjk* | 583 | 522 | 237 | **1.12x** | **faster** | 10 KB | identical |
-| scroll-region | 865 | 782 | 58 | **1.11x** | **faster** | 0 | identical |
-| cursor-motion | 188 | 172 | 95 | **1.09x** | **faster** | 32 KB | identical |
-
-\* From a later invocation of those two workloads alone. In the full run they
-landed in a noisy stretch (spreads over 50%) and measured 0.98x and 1.22x.
-Other quiet runs of the same code measured 1.07–1.21x on cjk and 1.18x on
-emoji.
-
-`ghostty (DT)` skips projection without `--frame-kb`. The one-row difference in
-`styled` is a behavior difference, not a measurement artifact: builtin drops a
-combining mark that follows a space, and Ghostty keeps it.
-
-How builtin got here, as the median builtin/ghostty-vt ratio per round (each
-round measured against its own in-run Ghostty control):
-
-| Workload | `dc1cd26` | round 1 | round 2 (packed cells) | round 3 | round 4 (`f45b95b`) | round 5 |
-| --- | ---: | ---: | ---: | ---: | ---: | ---: |
-| ascii | 0.009 | 0.20 | 0.55 | 1.21 | 1.24 | 1.18 |
-| ascii-lines | 0.009 | 0.20 | 0.50 | 1.13 | 1.19 | 1.14 |
-| light-cells | 0.009 | 0.32 | 0.48 | 1.20 | 1.23 | 1.30 |
-| scrolling | 0.015 | 0.14 | 0.40 | 1.11 | 1.33 | 1.73 |
-| dense-cells | 0.42 | 0.63 | 0.62 | 1.57 | 1.44 | 1.90 |
-| styled | 0.30 | 1.33 | 1.14 | 2.82 | 2.91 | 2.54 |
-| osc | 0.29 | 2.60 | 4.52 | 4.57 | 4.59 | 5.82 |
-| mixed | 0.03 | 0.39 | 0.45 | 1.56 | 1.84 | 2.15 |
-| scroll-region | 0.018 | 0.22 | 0.34 | 0.90 | 0.96 | 1.11 |
-| cursor-motion | 0.11 | 0.23 | 0.17 | 0.94 | 0.90 | 1.09 |
-| utf8 | 0.10 | 0.36 | 0.27 | 0.57 | 0.77 | 1.36 |
-| cjk | — | — | — | 0.53 | 0.77 | 1.12 |
-| emoji | — | — | — | 0.50 | 0.80 | 1.21 |
-
-Round 5 was measured with 3 s samples; earlier rounds used 500 ms samples.
-
-The `-Baseline` gate flagged real regressions twice:
-
-- **Round 2:** utf8 and cursor-motion.
-- **Round 4:** dense-cells and cursor-motion, from caching packed templates on
- the short-ASCII path.
-
-Each was confirmed with an interleaved A/B build and fixed before moving on.
-Round 5 passed the gate after each change. Moves of about ±10% between rounds
-(ascii and ascii-lines into round 5, for example) are within this machine's
-noise. styled's 2.54x comes from a run with a 136% spread; single-workload
-runs of the same code measured 3.39–3.61x.
-
-### DT's Ghostty mode vs raw `libghostty-vt`, with frames
-
-These numbers use `--frame-kb 256`, so a projection is taken every 256 KiB.
-They were measured in round 4; round 5 did not change the Ghostty engine.
-
-| Workload | ghostty (DT) before | after | ghostty-vt | ratio before → after |
-| --- | ---: | ---: | ---: | ---: |
-| ascii | 49.8 | 214.4 | 960 | 0.05 → 0.22 |
-| ascii-lines | 46.5 | 220.0 | 660 | 0.07 → 0.33 |
-| cursor-motion | 36.9 | 79.9 | 146 | 0.25 → 0.55 |
-| mixed | 41.1 | 146.5 | 295 | 0.12 → 0.50 |
-| light-cells | 45.6 | 57.5 | 1,580 | 0.03 → 0.04 |
-
-`light-cells` rewrites every row between frames, so every row is dirty and the
-per-cell P/Invoke projection still dominates.
-
-### Frame preparation and CPU paint
-
-`render`, 120x30, median p50 / p95, measured in round 4:
-
-| Stage | before packed cells | now |
-| --- | --- | --- |
-| snapshot | 0.009 / 0.011 ms | 0.040 / 0.044 ms |
-| snapshot + plan | 0.074 / 0.082 ms | 0.113 / 0.129 ms |
-| static paint | 0.69 / 1.08 ms | 0.75 / 1.00 ms |
-| one-row update | 0.69 / 1.70 ms | 0.76 / 1.06 ms |
-| scrolling paint | 3.29 / 9.72 ms | 2.90 / 5.54 ms |
-
-Snapshots now unpack each packed cell into a public `Cell`, which costs about
-0.03 ms per 120x30 frame. Every stage still fits well inside a 16.7 ms frame.
-No Ghostty counterpart exists on Windows.
-
-### What changed in the builtin engine
-
-Every fast path is checked against the per-character path by differential
-tests (`AsciiFastPathTests`, `CsiFastPathTests`, `CellRowModelTests`,
-`PackedCellStorageTests`, `CombiningTableTests`). Each new branch was also
-checked by deliberately breaking it and confirming a test fails.
-
-- **Packed, reference-free storage** (`CellRow`, `PackedCell`).
- - A cell is 16 bytes with no GC references, down from about 40 with two
- `string?` fields.
- - Hyperlinks live in a per-line side array that exists only while needed.
- - Combining text is interned per buffer (`CombiningTable`), and lines store
- int IDs. A cluster seen before costs no allocation, a recycled line keeps
- its ID array, and the table compacts so ever-new clusters cannot grow it
- without bound. This took utf8 from 9.4 MB to under 0.1 MB allocated per
- MiB.
- - The public `Cell` and `TermColor` are unchanged, and every value round-trips
- losslessly.
-- **Logically blank cells are never written.**
- - A line stores only its written range; everything outside it reads as the
- blank it was cleared with.
- - Recycling a scrollback line, the coldest memory in the buffer, is O(1).
- - Erasing to the end of a line is O(1) for left-aligned text.
- - Writes fill only the gap between the stored range and the new cells.
-- **Bulk printing.**
- - **ASCII:** the parser hands whole printable-ASCII runs to
- `IVtDispatch.PrintAscii`. They are stored a row segment at a time, two
- 64-bit (or, with AVX2, two 256-bit per four cells) stores per cell batch.
- - **UTF-8 runs:** well-formed UTF-8 runs go to `IVtDispatch.PrintRunes`.
- Runs of 3- or 4-byte sequences (CJK, emoji) are decoded four at a time
- from 16-byte `Vector128` loads, with the scalar decoder's validity rules
- (no overlong forms, surrogates or values past U+10FFFF); anything else
- falls back to the scalar decoder. On CJK this decodes about 3x faster.
- - **Grapheme joins:** precomputed tables for planes 0 and 1 rule out joins,
- so runes that cannot join are stored a segment at a time. That includes
- emoji after a cell without a trailing ZWJ. A zero-width rune with no join
- property (a combining accent, ZWJ) goes straight into the previous cell's
- combining text.
- - **Rows without wide glyphs:** a conservative per-row "may hold wide cells"
- flag means classifying and storing happen in one pass, with no clearing
- around wide glyphs. Every freshly recycled scrollback line qualifies. That
- pass ORs each rune into precomputed template bits and writes one 16-byte
- store per cell, with widths from a table where ambiguous widths are already
- resolved.
- - **Short writes:** the packed print templates are cached and rebuilt only
- when the attributes, protection or shell-integration state change, and a
- single printable byte between escape sequences is one store.
-- **CSI fast path.** A complete plain CSI sequence is parsed in one pass,
- straight into the arrays the dispatcher receives, and the parser calls the
- engine directly instead of through `IVtDispatch`. Sequences with no private
- marker or intermediate byte skip the special cases and go straight to the
- switch on the final byte.
-- **C0 fast path.** CR, LF and other controls in ground state go straight to
- `ExecuteC0` instead of through the string-state checks.
-- **Scroll regions** rotate line references with `Array.Copy`, not one
- indexer call per line.
-
-### What still separates builtin from Ghostty
-
-Builtin now leads on every workload. The closest leads, `cursor-motion`,
-`scroll-region` and `cjk` (1.09–1.12x), sit near the ±10% band of this
-machine's noise, and `ascii-lines` and `utf8` still get `overlap` verdicts
-from run-to-run spread. Timed stage by stage on CJK after round 5 (one engine
-reused with history full, per scalar, all stages in one process):
-
-| Stage | Cost |
-| --- | --- |
-| UTF-8 decode, block path | ~0.5–1.0 ns |
-| Parser per-line work (CR/LF dispatch, run setup) | ~0.7–1 ns |
-| Cell stores (two 16-byte cells per wide character) | ~2 ns |
-| Buffer CR/LF, scrolling and line reuse | ~0.7 ns |
-
-- **Parser per-line overhead** now costs about as much as the decode. Each
- CR and LF still goes through an interface call, and each line's run of
- characters pays its setup once.
-- **Cell size.** Packed cells are 16 bytes against Ghostty's 8, so storing,
- clearing or copying a row moves twice the memory. Getting to 8 bytes means
- interning styles, which is a larger change.
-- **What did not help.** Prefetching the scrollback lines about to be reused
- measured slower: with history full, steady-state throughput is the same at
- 100 and 9,000 lines, so cold recycled lines are not a bottleneck.
-
-**Variance.** Within isolated processes the ratio repeats to about ±10% on
-this machine with 3 s samples. With 500 ms samples, runs straddle the CPU's
-turbo drop and single workloads swing by 50% or more. In one long process
-running every workload, ratios also drifted by up to 50%, because heap state
-carried over between workloads. For that reason the script runs one process
-per workload, and `-Baseline` only compares workloads with the same SHA-256
-and gates on the ratio, not on absolute MB/s.
-
-## Definition of "matches or beats Ghostty"
-
-A claim needs every row below. A missing or failed row means the claim has
-not been established.
-
-1. `vt`: `faster` (or, for "matches", `overlap` with a median ratio ≥ 0.95)
- on every workload, with identical screens, on at least one x64 and one
- arm64 host.
-2. The same corpora through upstream `ghostty-bench +terminal-stream` do not
- contradict layer 1. This rules out a P/Invoke artifact.
-3. Scrolling allocation is bounded. Steady-state `scrolling` should allocate
- close to 0 B/MiB once history is full, as Ghostty's page reuse does.
-4. On Linux/macOS, `time cat` and vtebench place DT at or above Ghostty, run
- under the conditions described in the end-to-end section.
-5. Frame latency and dropped frames: measure these even though Ghostty does
- not publish them. Input latency (key to PTY echo to presented frame) is
- unmeasured on both sides, and a DT measurement would be new evidence rather
- than a comparison.
+The script runs one process per `vt` workload: in one long process running
+every workload, ratios drifted by up to 50% because heap state carried over
+between workloads. `-Baseline` only compares workloads with the same SHA-256
+(use the same `-Quick` and `-Workloads` settings as the baseline) and gates on
+the builtin/control ratio, not on absolute MB/s. `-RegressionPercent` changes
+the 15% threshold.
Run `scripts/Invoke-TerminalBenchmarks.ps1 -Baseline` on every engine change
so wins are kept, and record the commit, machine and `vt.json` with each
diff --git a/docs/release.md b/docs/release.md
index 412a0c1..55245fe 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -26,12 +26,10 @@ Unix RID-specific publishes remove the Windows-only `OpenConsole.exe` hosts from
ConPTY uses asynchronous host-side pipes and registered process-exit waits, with synchronous pipe endpoints for the console host, so idle sessions and blocked input do not exhaust the worker pool.
Linux and macOS local sessions use the
bundled `forkpty` relay. The Avalonia shell, settings, renderer, and terminal
-engines are shared.
+engine are shared.
CI workflows:
-- `build-ghostty.yml` — compile `libghostty-vt` for every RID and upload
- artifacts (optional cache; not required to develop).
- `build-terminal.yml` — restore natives from source, test, NativeAOT desktop, Linux
packages, macOS `.app`/zip, MSIX, the `Devolutions.Terminal.Control`
NuGet package, and the browser WASM host. NativeAOT desktop releases include
@@ -110,7 +108,7 @@ pwsh scripts/Test-MacOsRuntime.ps1 artifacts/packages
```
The staged `Devolutions Terminal.app` contains `Devolutions.Terminal`, `dt`,
-`dt-pty-host`, `libghostty-vt.dylib`, Skia, HarfBuzz, `Info.plist`, and an
+`dt-pty-host`, Skia, HarfBuzz, `Info.plist`, and an
ad-hoc signed icon. This gate (`Build-MacOsPackage.ps1`) produces an unsigned,
unnotarized zip for fast local iteration; the CI release pipeline additionally
runs `scripts/Release-MacOsPackage.ps1` to sign every Mach-O with a Developer ID
@@ -128,7 +126,7 @@ scripts/Build-LinuxPackage.sh linux-arm64 2026.3.0 artifacts/packages all
These commands publish the default self-contained desktop and create reproducible tar, DEB,
RPM, and AppImage artifacts plus a SHA-256 manifest. Every format is staged from
one canonical filesystem root and includes `Devolutions.Terminal`, `dt`,
-`dt-pty-host`, Ghostty, Skia, HarfBuzz, licenses, freedesktop metadata, icons,
+`dt-pty-host`, Skia, HarfBuzz, licenses, freedesktop metadata, icons,
the reversible integration helper, a sorted inventory, and an SPDX 2.3 SBOM.
Set `SOURCE_DATE_EPOCH` for release reproducibility. To reuse a separately
validated publish, set `LINUX_PUBLISH_DIR`.
@@ -159,7 +157,7 @@ uninstall removes every owned file. When installed, `desktop-file-validate` and
`Test-LinuxArm64Runtime.sh` must run on native Linux ARM64 (`uname -m` equal to
`aarch64` or `arm64`) and deliberately rejects x64 and QEMU-based
cross-execution. It runs only non-UI gates: NativeAOT `dt` startup/parser,
-Ghostty ABI and feed, the built-in engine, real `forkpty` lifecycle, broker
+the built-in engine, real `forkpty` lifecycle, broker
concurrency, Linux profile/XDG discovery, and package lifecycle under a
disposable root. CI uses the exact GitHub-hosted runner label
`ubuntu-24.04-arm` in job `linux-arm64-hardware`. If that label is unavailable
diff --git a/docs/renderer-benchmarks.md b/docs/renderer-benchmarks.md
index 77dda02..7542dd5 100644
--- a/docs/renderer-benchmarks.md
+++ b/docs/renderer-benchmarks.md
@@ -4,8 +4,7 @@ The `render` mode of `tools/Devolutions.Terminal.Bench` measures the CPU work of
preparing and rasterizing terminal frames. It is not a GPU or presentation
benchmark. The existing `engine` and `control` modes measure different parts
of the pipeline; `control` uses Avalonia headless and does **not** paint.
-For the engine-level comparison against Ghostty (`vt` mode), the full suite
-script and the "beats Ghostty" criteria, see
+For engine-level VT ingest (`vt` mode) and the full suite script, see
[performance benchmarks](performance-benchmarks.md).
## Reproducible input
@@ -79,8 +78,8 @@ and three Release runs, the median-run CPU-raster results were:
| Scrolling p50 / p95 | 35.512 / 52.286 ms | 3.435 / 5.932 ms |
| Scrolling managed allocation/frame | 3,428,490 B | 157,920 B |
-These are separate runs on one machine, not a speed ratio against Ghostty.
-In this workload 16 KiB updates replace most visible rows; the row cache
+These are separate runs on one machine, not a speed ratio against another
+terminal. In this workload 16 KiB updates replace most visible rows; the row cache
therefore helps static/localized paints more than scrolling. Replaying only
eight warmed frames gives artificially high row-cache hit rates and is not a
substitute for the longer scrolling case.
@@ -152,38 +151,6 @@ at the default 4,096-glyph bound. Median-run p50 changed from 3.388 to
from these runs. For fully warmed static and localized paints, use at least
`--warmup 2` so both the candidate and picture passes finish before timing.
-## Ghostty reference measurements
-
-This repository pins Ghostty at the commit in
-`native/ghostty/ghostty-upstream.json`. Build **that** source with Zig 0.16.0
-using `zig build -Demit-bench -Doptimize=ReleaseFast` (on macOS add
-`-Demit-macos-app=false`). With the pinned revision on Windows, build the
-benchmark-only/lib-vt targets instead; the default full target currently
-fails to link because of a duplicate `memset` symbol:
-
-```powershell
-zig build -Demit-bench=true -Demit-lib-vt=true -Doptimize=ReleaseFast -j1
-```
-
-The upstream `zig-out/bin/ghostty-bench` (`.exe` on Windows) can consume the
-exported corpus:
-
-```text
-ghostty-bench +screen-clone --mode=render-partial --terminal-rows=30 --terminal-cols=120 --data=
-```
-
-Run it repeatedly with warmups (e.g. `hyperfine`) rather than timing
-`ghostty-gen | ghostty-bench`: keep generation out of the measurement and use
-enough internal operations to amortize process startup and input loading.
-Ghostty's `screen-clone` `render`, `render-clean`, and
-`render-partial` modes time updates to its CPU `RenderState`, **not** GPU
-painting. Their steps respectively perform 50,000 full, 3,000,000 clean, or
-2,000,000 partial updates at `--loops=1`. External wall times also include
-process startup and corpus loading. They use different semantics and
-iteration counts from this project's snapshot/plan stage; record them as
-separate diagnostics, not a numeric speed ratio. The `libghostty-vt` binary
-used by this project has no Ghostty graphics renderer.
-
## Full renderer comparison (Linux/macOS)
For a user-visible comparison, benchmark the full GUI applications on the
@@ -196,8 +163,8 @@ scrolling separately; verify the rendered screen after each run, and exclude
unsupported image protocols from paired workloads.
Instrument Avalonia at `TermControl.Render` (snapshot/plan), then at
-`TerminalSkiaDrawOperation.Render` (Skia submission). Instrument upstream
-Ghostty at `RenderState.update` and `renderer/generic.zig:drawFrame`. Both draw
+`TerminalSkiaDrawOperation.Render` (Skia submission), and instrument the other
+terminal at its equivalent frame-preparation and draw entry points. Both draw
paths may return before presentation: use platform GPU/compositor tracing or
presentation timestamps for end-to-end frame latency and dropped frames.
Report CPU preparation, render submission, GPU completion/presentation, p50/
@@ -205,11 +172,9 @@ p95/p99 frame intervals, and CPU/GPU utilization **as distinct metrics**.
Full-app elapsed time also includes each app's engine, transport, and
scheduling; it is not a render-only speed ratio.
Warm up caches, alternate run order, collect repeated runs, and never run both
-apps' measurements concurrently. Ghostty's native GUI is available on
-Linux/macOS, not Windows; its Windows `libghostty-vt` support cannot stand in
-for a native renderer comparison.
+apps' measurements concurrently.
-For a claim of beating Ghostty **at every measured point**, establish separate
+For a claim of beating another terminal **at every measured point**, establish separate
equivalent baselines for VT ingest, CPU frame preparation, idle/static paint,
localized updates, first-pass and replayed scrolling, and presented frame
latency. Compare p50, p95, p99, allocation/peak resident memory, CPU/GPU
@@ -217,5 +182,9 @@ utilization, and dropped frames for each workload on the same native host;
require pixel or terminal-state equivalence first. Alternate the applications
across repeated runs and report the run-to-run spread as well as medians. A
failure or unmeasured metric means the all-points claim has **not** passed.
-The Windows offscreen raster and upstream `screen-clone` results above measure
-different work and cannot satisfy the presented-frame gate.
+The Windows offscreen raster results above measure different work and cannot
+satisfy the presented-frame gate.
+
+Earlier comparison measurements against another engine, from before this
+project had a single engine, are kept in the history record linked from
+[performance benchmarks](performance-benchmarks.md).
diff --git a/docs/renderer.md b/docs/renderer.md
index af349bb..b8f3597 100644
--- a/docs/renderer.md
+++ b/docs/renderer.md
@@ -69,7 +69,7 @@ Coalescing takes an 8 MiB burst from 514 drains to roughly one per frame.
The separate `render` mode measures snapshot/planning and CPU Skia rasterization
of static, one-row-update, and scrolling screens. See
[renderer benchmarks](renderer-benchmarks.md) for reproducible inputs, results
-and the limits of comparing those measurements with Ghostty.
+and the limits of comparing those measurements with other terminals.
## Performance contract
diff --git a/linux/com.devolutions.Terminal.metainfo.xml b/linux/com.devolutions.Terminal.metainfo.xml
index e217615..8965b68 100644
--- a/linux/com.devolutions.Terminal.metainfo.xml
+++ b/linux/com.devolutions.Terminal.metainfo.xml
@@ -8,7 +8,7 @@
Devolutions Terminal is a cross-platform Avalonia terminal emulator
- with tabs, panes, configurable profiles, and built-in or Ghostty terminal
+ with tabs, panes, configurable profiles, and built-in terminal
engines.
diff --git a/macos/entitlements.plist b/macos/entitlements.plist
index 0198393..7665810 100644
--- a/macos/entitlements.plist
+++ b/macos/entitlements.plist
@@ -7,7 +7,7 @@
com.apple.security.cs.allow-unsigned-executable-memory
-
com.apple.security.cs.disable-library-validation
diff --git a/macos/package.env b/macos/package.env
index 7e9021f..8de9cb1 100644
--- a/macos/package.env
+++ b/macos/package.env
@@ -8,7 +8,6 @@ BUNDLE_NAME="Devolutions Terminal.app"
EXECUTABLE_NAME=Devolutions.Terminal
CLI_NAME=dt
PTY_HOST_NAME=dt-pty-host
-GHOSTTY_LIBRARY=libghostty-vt.dylib
ICON_NAME=DevolutionsTerminal
MACOS_DEPLOYMENT_TARGET=13.0
LICENSE_ID="MIT AND OFL-1.1"
diff --git a/native/NativeLibraries.targets b/native/NativeLibraries.targets
index 2b71476..61beb2d 100644
--- a/native/NativeLibraries.targets
+++ b/native/NativeLibraries.targets
@@ -1,60 +1,46 @@
$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))
- $(RuntimeIdentifier)
- $(NETCoreSdkPortableRuntimeIdentifier)
- libghostty-vt.so
- libghostty-vt.dylib
- ghostty-vt.dll$(RuntimeIdentifier)
- $(NETCoreSdkPortableRuntimeIdentifier)
- all
+ $(NETCoreSdkPortableRuntimeIdentifier)
+ ptypowershell.exepwshpwsh
+
+ Condition="'$(SkipNativeRestore)' != 'true' and '$(DesignTimeBuild)' != 'true' and '$(NativeRestore)' != 'none' and '$(UnixPtyRid)' != ''">
-
+ Condition="'$(NativeRestore)' != 'none' and '$(UnixPtyRid)' != ''">
+ Condition="Exists('$(NativeRoot)linux-pty\$(UnixPtyRid)\dt-pty-host')" />
+ Condition="Exists('$(TargetDir)dt-pty-host') and !$([MSBuild]::IsOSPlatform('Windows'))" />
-
+ Condition="'$(NativeRestore)' != 'none' and '$(PublishDir)' != '' and '$(UnixPtyRid)' != ''">
+ Condition="Exists('$(NativeRoot)linux-pty\$(UnixPtyRid)\dt-pty-host')" />
+ Condition="Exists('$(PublishDir)dt-pty-host') and !$([MSBuild]::IsOSPlatform('Windows'))" />
diff --git a/native/Restore-NativeLibraries.ps1 b/native/Restore-NativeLibraries.ps1
index 5542778..e3a8b37 100644
--- a/native/Restore-NativeLibraries.ps1
+++ b/native/Restore-NativeLibraries.ps1
@@ -1,8 +1,6 @@
[CmdletBinding()]
param(
[string] $Rid,
- [ValidateSet("all", "ghostty", "pty")]
- [string] $Component = "all",
[string] $ZigPath,
[switch] $Force
)
@@ -14,20 +12,17 @@ $ErrorActionPreference = "Stop"
$nativeRoot = $PSScriptRoot
$dotnetRoot = [IO.Path]::GetFullPath((Join-Path $nativeRoot ".."))
-$manifest = Get-Content -LiteralPath (Join-Path $nativeRoot "ghostty\ghostty-upstream.json") -Raw |
- ConvertFrom-Json
-$zigVersion = [string]$manifest.zig
+$zigVersion = Get-PinnedZigVersion
if ([string]::IsNullOrWhiteSpace($Rid)) {
$Rid = Get-HostRid
}
-$buildGhostty = $Component -in @("all", "ghostty")
+# dt-pty-host is the only native helper built from source, and only Unix RIDs use it.
$isUnixRid = $Rid.StartsWith("linux-", [StringComparison]::Ordinal) -or
$Rid.StartsWith("osx-", [StringComparison]::Ordinal)
-$buildPty = ($Component -in @("all", "pty")) -and $isUnixRid
-if (-not $buildGhostty -and -not $buildPty) {
- Write-Host "No native libraries required for $Rid ($Component)."
+if (-not $isUnixRid) {
+ Write-Host "No native libraries required for $Rid."
return
}
@@ -35,64 +30,24 @@ $mutex = [System.Threading.Mutex]::new($false, "Devolutions.Terminal.NativeResto
$acquired = $false
try {
$acquired = $mutex.WaitOne()
- $needsBuild = $false
- if ($buildGhostty) {
- $fileName = [string]$manifest.targets.$Rid.file
- if ($Force -or -not (Test-Path -LiteralPath (Join-Path $nativeRoot "ghostty\$Rid\$fileName"))) {
- $needsBuild = $true
+ $output = Join-Path $nativeRoot "linux-pty\$Rid\dt-pty-host"
+ if ($Force -or -not (Test-Path -LiteralPath $output)) {
+ $ptyArgs = @{
+ Rid = $Rid
+ ZigPath = Resolve-ZigPath -ZigPath $ZigPath -Version $zigVersion -DotnetRoot $dotnetRoot -Install
}
- }
-
- if ($buildPty -and ($Force -or -not (Test-Path -LiteralPath (Join-Path $nativeRoot "linux-pty\$Rid\dt-pty-host")))) {
- $needsBuild = $true
- }
-
- if ($needsBuild) {
- $ZigPath = Resolve-ZigPath -ZigPath $ZigPath -Version $zigVersion -DotnetRoot $dotnetRoot -Install
- }
-
- if ($buildGhostty) {
- $fileName = [string]$manifest.targets.$Rid.file
- $output = Join-Path $nativeRoot "ghostty\$Rid\$fileName"
- if ($Force -or -not (Test-Path -LiteralPath $output)) {
- $ghosttyArgs = @{
- Rid = $Rid
- ZigPath = $ZigPath
- }
- if ($Force) {
- $ghosttyArgs.Force = $true
- }
-
- & (Join-Path (Join-Path $nativeRoot "ghostty") "Build-Ghostty.ps1") @ghosttyArgs
- if ($LASTEXITCODE -ne 0) {
- throw "Build-Ghostty.ps1 failed for $Rid."
- }
- }
- else {
- Write-Host "Using existing $output"
+ if ($Force) {
+ $ptyArgs.Force = $true
}
- }
- if ($buildPty) {
- $output = Join-Path $nativeRoot "linux-pty\$Rid\dt-pty-host"
- if ($Force -or -not (Test-Path -LiteralPath $output)) {
- $ptyArgs = @{
- Rid = $Rid
- ZigPath = $ZigPath
- }
- if ($Force) {
- $ptyArgs.Force = $true
- }
-
- & (Join-Path (Join-Path $nativeRoot "linux-pty") "Build-LinuxPtyHost.ps1") @ptyArgs
- if ($LASTEXITCODE -ne 0) {
- throw "Build-LinuxPtyHost.ps1 failed for $Rid."
- }
- }
- else {
- Write-Host "Using existing $output"
+ & (Join-Path (Join-Path $nativeRoot "linux-pty") "Build-LinuxPtyHost.ps1") @ptyArgs
+ if ($LASTEXITCODE -ne 0) {
+ throw "Build-LinuxPtyHost.ps1 failed for $Rid."
}
}
+ else {
+ Write-Host "Using existing $output"
+ }
}
finally {
if ($acquired) {
diff --git a/native/Zig.ps1 b/native/Zig.ps1
index a8db016..38efa9b 100644
--- a/native/Zig.ps1
+++ b/native/Zig.ps1
@@ -1,6 +1,12 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
+# The Zig release that builds the native helpers, pinned in native/zig.json.
+function Get-PinnedZigVersion {
+ $manifest = Get-Content -LiteralPath (Join-Path $PSScriptRoot "zig.json") -Raw | ConvertFrom-Json
+ return [string]$manifest.zig
+}
+
function Test-HostWindows {
return [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows)
diff --git a/native/ghostty/Build-Ghostty.ps1 b/native/ghostty/Build-Ghostty.ps1
deleted file mode 100644
index 7e9476f..0000000
--- a/native/ghostty/Build-Ghostty.ps1
+++ /dev/null
@@ -1,265 +0,0 @@
-[CmdletBinding()]
-param(
- [string[]] $Rid = @(),
- [string] $ZigPath,
- [string] $SourceDirectory,
- [string] $LlvmStripPath,
- [switch] $InstallZig,
- [switch] $SkipHashCheck,
- [switch] $VerifyHash,
- [switch] $SkipCopy,
- [switch] $Force
-)
-
-Set-StrictMode -Version Latest
-$ErrorActionPreference = "Stop"
-
-. (Join-Path $PSScriptRoot "..\Zig.ps1")
-
-$nativeRoot = $PSScriptRoot
-$manifestPath = Join-Path $nativeRoot "ghostty-upstream.json"
-$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
-$commit = [string]$manifest.commit
-$zigVersion = [string]$manifest.zig
-$dotnetRoot = [IO.Path]::GetFullPath((Join-Path $nativeRoot "..\.."))
-$artifacts = Join-Path $dotnetRoot "artifacts"
-$SkipHashCheck = $SkipHashCheck -or -not $VerifyHash
-
-function Resolve-LlvmStrip {
- param([string] $Explicit)
-
- if (-not [string]::IsNullOrWhiteSpace($Explicit) -and (Test-Path -LiteralPath $Explicit)) {
- return $Explicit
- }
-
- $command = Get-Command llvm-strip -ErrorAction SilentlyContinue | Select-Object -First 1
- if ($null -ne $command) {
- return $command.Source
- }
-
- if ((Test-HostWindows) -and $null -ne $env:ProgramFiles) {
- $vsStrip = Get-ChildItem -File -ErrorAction SilentlyContinue -Path (
- Join-Path $env:ProgramFiles "Microsoft Visual Studio\*\*\VC\Tools\Llvm\x64\bin\llvm-strip.exe"
- ) | Select-Object -First 1
- if ($null -ne $vsStrip) {
- return $vsStrip.FullName
- }
- }
-
- if (Test-HostMacOS) {
- $xcrun = Get-Command xcrun -ErrorAction SilentlyContinue
- if ($null -ne $xcrun) {
- $fromXcode = & xcrun --find llvm-strip 2>$null
- if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($fromXcode)) {
- return $fromXcode.Trim()
- }
- }
- }
-
- $strip = Get-Command strip -ErrorAction SilentlyContinue | Select-Object -First 1
- if ($null -ne $strip) {
- return $strip.Source
- }
-
- return $null
-}
-
-function Find-BuiltLibrary {
- param(
- [string] $Prefix,
- [string] $FileName
- )
-
- $candidates = @(
- (Join-Path $Prefix "bin\$FileName"),
- (Join-Path $Prefix "lib\$FileName"),
- (Join-Path $Prefix "lib\$FileName.0.1.0")
- )
- foreach ($candidate in $candidates) {
- if (Test-Path -LiteralPath $candidate) {
- return $candidate
- }
- }
-
- $search = Get-ChildItem -LiteralPath (Join-Path $Prefix "lib") -ErrorAction SilentlyContinue |
- Where-Object { $_.Name -like "$FileName*" } |
- Select-Object -First 1
- if ($null -ne $search) {
- return $search.FullName
- }
-
- throw "Zig build finished but '$FileName' was not found under '$Prefix'."
-}
-
-if ([string]::IsNullOrWhiteSpace($SourceDirectory)) {
- $SourceDirectory = Join-Path $artifacts "ghostty-src"
-}
-
-$ZigPath = Resolve-ZigPath -ZigPath $ZigPath -Version $zigVersion -DotnetRoot $dotnetRoot -Install
-
-$selected = @()
-if ($Rid.Count -eq 0) {
- $selected = @(Get-HostRid)
-}
-else {
- foreach ($value in $Rid) {
- if (-not (@($manifest.targets.PSObject.Properties.Name) -contains $value)) {
- throw "Unknown RID '$value'. Known: $($manifest.targets.PSObject.Properties.Name -join ', ')."
- }
-
- $selected += $value
- }
-}
-
-$pending = @()
-foreach ($currentRid in $selected) {
- $fileName = [string]$manifest.targets.$currentRid.file
- $destination = Join-Path $nativeRoot "$currentRid\$fileName"
- if (-not $Force -and -not $SkipCopy -and (Test-Path -LiteralPath $destination)) {
- Write-Host "Skipping $currentRid (already built)"
- continue
- }
-
- $pending += $currentRid
-}
-
-if ($pending.Count -eq 0) {
- return
-}
-
-$selected = $pending
-
-if (-not (Test-Path -LiteralPath (Join-Path $SourceDirectory ".git"))) {
- New-Item -ItemType Directory -Force -Path (Split-Path -Parent $SourceDirectory) | Out-Null
- & git clone --filter=blob:none $manifest.repository $SourceDirectory
- if ($LASTEXITCODE -ne 0) {
- throw "Failed to clone Ghostty."
- }
-}
-
-& git -C $SourceDirectory fetch origin $commit
-& git -C $SourceDirectory checkout --detach $commit
-if ($LASTEXITCODE -ne 0) {
- throw "Failed to check out Ghostty commit $commit."
-}
-
-$hashes = [ordered]@{}
-foreach ($currentRid in $selected) {
- $target = $manifest.targets.$currentRid
- $zigTarget = [string]$target.zigTarget
- $fileName = [string]$target.file
- $prefix = Join-Path $artifacts "ghostty-native\$currentRid"
-
- if ($currentRid.StartsWith("osx-", [StringComparison]::Ordinal) -and (Test-HostMacOS)) {
- $sdk = & xcrun --show-sdk-path
- if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sdk)) {
- throw "xcrun --show-sdk-path failed; Xcode CLT is required for macOS libghostty-vt."
- }
-
- $env:SDKROOT = $sdk.Trim()
- }
-
- if (Test-Path -LiteralPath $prefix) {
- Remove-Item -LiteralPath $prefix -Recurse -Force
- }
-
- Push-Location $SourceDirectory
- try {
- & $ZigPath build `
- -Demit-lib-vt=true `
- "-Dtarget=$zigTarget" `
- "-Doptimize=$($manifest.optimize)" `
- -Dstrip=true `
- -Dsimd=true `
- --prefix $prefix
- if ($LASTEXITCODE -ne 0) {
- throw "Ghostty build failed for $currentRid."
- }
- }
- finally {
- Pop-Location
- }
-
- $built = Find-BuiltLibrary -Prefix $prefix -FileName $fileName
- $linux = $currentRid.StartsWith("linux-", [StringComparison]::Ordinal)
- if ($linux -and $manifest.strip) {
- $stripTool = Resolve-LlvmStrip -Explicit $LlvmStripPath
- if ($currentRid -eq "linux-arm64" -and
- [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne "Arm64") {
- $crossStrip = Get-Command aarch64-linux-gnu-strip -ErrorAction SilentlyContinue |
- Select-Object -First 1
- if ($null -ne $crossStrip) {
- $stripTool = $crossStrip.Source
- }
- elseif ($stripTool -and
- [IO.Path]::GetFileNameWithoutExtension($stripTool) -eq "strip") {
- $stripTool = $null
- }
- }
- if ([string]::IsNullOrWhiteSpace($stripTool)) {
- $requiresHash = @($target.PSObject.Properties.Name) -contains "sha256" -and
- -not [string]::IsNullOrWhiteSpace([string]$target.sha256)
- if (-not $SkipHashCheck -and $requiresHash) {
- throw "llvm-strip/strip was not found. Pass -LlvmStripPath or -SkipHashCheck."
- }
- }
- else {
- $stripped = "$built.stripped"
- & $stripTool --strip-debug -o $stripped $built
- if ($LASTEXITCODE -ne 0) {
- throw "Failed to strip debug data from $currentRid."
- }
-
- $built = $stripped
- }
- }
-
- $sha = [System.Security.Cryptography.SHA256]::Create()
- try {
- $stream = [IO.File]::OpenRead($built)
- try {
- $hash = [BitConverter]::ToString($sha.ComputeHash($stream)).Replace("-", "")
- }
- finally {
- $stream.Dispose()
- }
- }
- finally {
- $sha.Dispose()
- }
- $expected = $null
- if (@($target.PSObject.Properties.Name) -contains "sha256") {
- $expected = [string]$target.sha256
- }
-
- if (-not $SkipHashCheck -and -not [string]::IsNullOrWhiteSpace($expected) -and $hash -ne $expected) {
- throw "Unexpected $currentRid hash $hash. Expected $expected."
- }
-
- $hashes[$currentRid] = $hash
- Write-Host "$currentRid SHA-256 $hash"
-
- if (-not $SkipCopy) {
- $destinationDir = Join-Path $nativeRoot $currentRid
- New-Item -ItemType Directory -Force -Path $destinationDir | Out-Null
- $destination = Join-Path $destinationDir $fileName
- Copy-Item -LiteralPath $built -Destination $destination -Force
- Set-Content -LiteralPath "$destination.sha256" -Value $hash
- }
-}
-
-if ($env:GITHUB_STEP_SUMMARY) {
- $lines = @(
- "## libghostty-vt",
- "",
- "Commit ``$commit`` built with Zig $zigVersion.",
- "",
- "| RID | SHA-256 |",
- "| --- | --- |"
- )
- foreach ($entry in $hashes.GetEnumerator()) {
- $lines += "| ``$($entry.Key)`` | ``$($entry.Value)`` |"
- }
-
- Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value ($lines -join "`n")
-}
diff --git a/native/ghostty/LICENSE b/native/ghostty/LICENSE
deleted file mode 100644
index 0a07a66..0000000
--- a/native/ghostty/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/native/ghostty/README.md b/native/ghostty/README.md
deleted file mode 100644
index 7bdf0bd..0000000
--- a/native/ghostty/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# libghostty-vt native assets
-
-The optional Ghostty terminal engine uses `libghostty-vt` built from
-[`ghostty-upstream.json`](ghostty-upstream.json):
-
-- repository: `https://github.com/ghostty-org/ghostty`
-- commit: `3c1ef5b32fc5ea6b93d28493fabf193f595139cf`
-- Zig: `0.16.0`
-- configuration: `ReleaseFast`, SIMD enabled
-- license: MIT (see `LICENSE`)
-
-Binaries are **not** committed. `dotnet build` / `dotnet test` restore the host
-RID via [`../Restore-NativeLibraries.ps1`](../Restore-NativeLibraries.ps1),
-which downloads Zig 0.16.0 if needed and compiles `libghostty-vt` into
-`native/ghostty//` (gitignored). A `zig` on `PATH` is used only when it is
-Zig 0.16.x; any other version (for example a newer system install) is ignored
-and the pinned Zig is downloaded into `artifacts/tools` instead. ABI checks use the type manifest, not a
-file hash.
-
-Linux: glibc **2.31+**. macOS: **13.0+** (`aarch64-macos.13.0` /
-`x86_64-macos.13.0`). Windows: MSVC.
-
-## Rebuild locally
-
-```powershell
-# Host RID (also run automatically by MSBuild)
-./native/Restore-NativeLibraries.ps1
-
-# One RID
-./native/ghostty/Build-Ghostty.ps1 -Rid win-x64
-```
-
-The script clones the pinned commit into `artifacts/ghostty-src`.
-
-`build-ghostty.yml` still builds every RID and uploads `libghostty-vt` artifacts.
-Optional fast path: download that artifact and run
-`Restore-GhosttyNative.ps1 -SourceDirectory artifacts/libghostty-vt`.
diff --git a/native/ghostty/Restore-GhosttyNative.ps1 b/native/ghostty/Restore-GhosttyNative.ps1
deleted file mode 100644
index 7a9938e..0000000
--- a/native/ghostty/Restore-GhosttyNative.ps1
+++ /dev/null
@@ -1,62 +0,0 @@
-[CmdletBinding()]
-param(
- [Parameter(Mandatory)]
- [string] $SourceDirectory,
- [string] $Rid,
- [string] $NativeRoot = $PSScriptRoot
-)
-
-Set-StrictMode -Version Latest
-$ErrorActionPreference = "Stop"
-
-$manifest = Get-Content -LiteralPath (Join-Path $NativeRoot "ghostty-upstream.json") -Raw |
- ConvertFrom-Json
-
-function Copy-Rid {
- param(
- [string] $CurrentRid,
- [string] $From
- )
-
- $fileName = [string]$manifest.targets.$CurrentRid.file
- if ([string]::IsNullOrWhiteSpace($fileName)) {
- throw "Unknown Ghostty RID '$CurrentRid'."
- }
-
- $source = Join-Path $From $fileName
- if (-not (Test-Path -LiteralPath $source)) {
- $nested = Join-Path $From "$CurrentRid\$fileName"
- if (Test-Path -LiteralPath $nested) {
- $source = $nested
- }
- else {
- throw "Missing '$fileName' under '$From'."
- }
- }
-
- $destinationDir = Join-Path $NativeRoot $CurrentRid
- New-Item -ItemType Directory -Force -Path $destinationDir | Out-Null
- Copy-Item -LiteralPath $source -Destination (Join-Path $destinationDir $fileName) -Force
- $hashFile = "$source.sha256"
- if (Test-Path -LiteralPath $hashFile) {
- Copy-Item -LiteralPath $hashFile -Destination (Join-Path $destinationDir "$fileName.sha256") -Force
- }
-}
-
-if (-not [string]::IsNullOrWhiteSpace($Rid)) {
- Copy-Rid -CurrentRid $Rid -From $SourceDirectory
- return
-}
-
-$copied = 0
-foreach ($property in $manifest.targets.PSObject.Properties) {
- $candidate = Join-Path $SourceDirectory $property.Name
- if (Test-Path -LiteralPath $candidate) {
- Copy-Rid -CurrentRid $property.Name -From $candidate
- $copied++
- }
-}
-
-if ($copied -eq 0) {
- throw "No libghostty-vt RID folders were found under '$SourceDirectory'."
-}
diff --git a/native/ghostty/ghostty-upstream.json b/native/ghostty/ghostty-upstream.json
deleted file mode 100644
index 7165d60..0000000
--- a/native/ghostty/ghostty-upstream.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "repository": "https://github.com/ghostty-org/ghostty.git",
- "commit": "3c1ef5b32fc5ea6b93d28493fabf193f595139cf",
- "zig": "0.16.0",
- "optimize": "ReleaseFast",
- "simd": true,
- "strip": true,
- "targets": {
- "win-x64": {
- "zigTarget": "x86_64-windows-msvc",
- "file": "ghostty-vt.dll"
- },
- "win-arm64": {
- "zigTarget": "aarch64-windows-msvc",
- "file": "ghostty-vt.dll"
- },
- "linux-x64": {
- "zigTarget": "x86_64-linux-gnu.2.31",
- "file": "libghostty-vt.so"
- },
- "linux-arm64": {
- "zigTarget": "aarch64-linux-gnu.2.31",
- "file": "libghostty-vt.so"
- },
- "osx-arm64": {
- "zigTarget": "aarch64-macos.13.0",
- "file": "libghostty-vt.dylib"
- },
- "osx-x64": {
- "zigTarget": "x86_64-macos.13.0",
- "file": "libghostty-vt.dylib"
- }
- }
-}
diff --git a/native/linux-pty/Build-LinuxPtyHost.ps1 b/native/linux-pty/Build-LinuxPtyHost.ps1
index 218c120..2c807b0 100644
--- a/native/linux-pty/Build-LinuxPtyHost.ps1
+++ b/native/linux-pty/Build-LinuxPtyHost.ps1
@@ -12,9 +12,7 @@ $ErrorActionPreference = "Stop"
$nativeRoot = $PSScriptRoot
$dotnetRoot = [IO.Path]::GetFullPath((Join-Path $nativeRoot "..\.."))
-$manifest = Get-Content -LiteralPath (Join-Path $nativeRoot "..\ghostty\ghostty-upstream.json") -Raw |
- ConvertFrom-Json
-$ZigPath = Resolve-ZigPath -ZigPath $ZigPath -Version ([string]$manifest.zig) -DotnetRoot $dotnetRoot -Install
+$ZigPath = Resolve-ZigPath -ZigPath $ZigPath -Version (Get-PinnedZigVersion) -DotnetRoot $dotnetRoot -Install
$targets = [ordered]@{
"linux-x64" = "x86_64-linux-gnu.2.31"
diff --git a/native/zig.json b/native/zig.json
new file mode 100644
index 0000000..4a03ce1
--- /dev/null
+++ b/native/zig.json
@@ -0,0 +1,3 @@
+{
+ "zig": "0.16.0"
+}
diff --git a/scripts/Build-LinuxPackage.sh b/scripts/Build-LinuxPackage.sh
index 11310c4..ec832e6 100644
--- a/scripts/Build-LinuxPackage.sh
+++ b/scripts/Build-LinuxPackage.sh
@@ -115,7 +115,7 @@ else
fi
find "$publish_dir" -type f \( -name '*.dbg' -o -name '*.pdb' \) -delete
-for artifact in Devolutions.Terminal dt dt-pty-host libghostty-vt.so libSkiaSharp.so libHarfBuzzSharp.so; do
+for artifact in Devolutions.Terminal dt dt-pty-host libSkiaSharp.so libHarfBuzzSharp.so; do
[[ -f "$publish_dir/$artifact" ]] ||
{ echo "Publish output is missing $artifact." >&2; exit 70; }
if [[ "$artifact" == *.so || "$artifact" != "lib"* ]]; then
diff --git a/scripts/Build-MacOsPackage.ps1 b/scripts/Build-MacOsPackage.ps1
index da1aada..da56479 100644
--- a/scripts/Build-MacOsPackage.ps1
+++ b/scripts/Build-MacOsPackage.ps1
@@ -86,7 +86,7 @@ try {
Remove-Item -Force
$artifacts = @(
- $metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME, $metadata.GHOSTTY_LIBRARY,
+ $metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME,
'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
)
foreach ($artifact in $artifacts) {
@@ -126,8 +126,7 @@ try {
"$base.zip",
"$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.EXECUTABLE_NAME)",
"$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.CLI_NAME)",
- "$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.PTY_HOST_NAME)",
- "$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.GHOSTTY_LIBRARY)"
+ "$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.PTY_HOST_NAME)"
)
$manifest = Get-Sha256Manifest -WorkingDirectory $OutputDir -Path $manifestPaths
Set-Content -LiteralPath (Join-Path $OutputDir "$base.sha256") -Value $manifest -Encoding utf8
diff --git a/scripts/Invoke-TerminalBenchmarks.ps1 b/scripts/Invoke-TerminalBenchmarks.ps1
index f3d0d7f..91c4b39 100644
--- a/scripts/Invoke-TerminalBenchmarks.ps1
+++ b/scripts/Invoke-TerminalBenchmarks.ps1
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
- # vt: builtin vs Ghostty ingest; render: frame prep + CPU raster; control: PTY->UI coalescing.
+ # vt: builtin ingest vs an in-run clock control; render: frame prep + CPU raster; control: PTY->UI coalescing.
[string[]] $Suite = @("vt", "render"),
# Smaller corpora and fewer runs for a fast local signal (not for published numbers).
@@ -25,14 +25,7 @@ param(
# Laptop CPUs drop out of turbo a few seconds into sustained load; samples of
# ~3000 ms all run at the sustained clock, so medians and spreads stay stable.
[ValidateRange(0, 600000)]
- [int] $MinMs = 0,
-
- # Upstream ghostty-bench built from the pinned commit (see docs/performance-benchmarks.md).
- # When given, every vt workload is also replayed through `+terminal-stream`.
- [string] $GhosttyBench,
-
- # Fail when builtin is not faster than raw libghostty-vt on every workload.
- [switch] $RequireBeatGhostty
+ [int] $MinMs = 0
)
Set-StrictMode -Version Latest
@@ -111,12 +104,8 @@ if ($Suite -contains "vt") {
$vtArgs += @("--min-ms", $MinMs)
}
- if (-not $RequireBeatGhostty) {
- $vtArgs += "--no-gate"
- }
-
if ((Invoke-Bench -Name "vt-$name" -Arguments $vtArgs) -ne 0) {
- $failures.Add("builtin did not beat ghostty-vt on vt workload '$name'")
+ $failures.Add("vt benchmark failed on workload '$name'")
}
$report = Get-Content -LiteralPath $partial -Raw | ConvertFrom-Json
@@ -130,33 +119,6 @@ if ($Suite -contains "vt") {
$merged | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $vtJson
- if ($GhosttyBench) {
- $corpora = Join-Path $OutputDirectory "corpora"
- Invoke-Bench -Name "vt-export" -Arguments @("vt", "--mb", $megabytes, "--workloads", $Workloads, "--export", $corpora) | Out-Null
- $upstream = foreach ($file in Get-ChildItem -LiteralPath $corpora -Filter "*.vt") {
- $times = foreach ($run in 1..$runs) {
- $watch = [System.Diagnostics.Stopwatch]::StartNew()
- & $GhosttyBench "+terminal-stream" "--data=$($file.FullName)" "--terminal-rows=30" "--terminal-cols=120" | Out-Null
- $watch.Stop()
- if ($LASTEXITCODE -ne 0) {
- throw "ghostty-bench failed on $($file.Name)."
- }
-
- $watch.Elapsed.TotalSeconds
- }
-
- $sorted = @($times | Sort-Object)
- $median = $sorted[[int][Math]::Floor($sorted.Count / 2)]
- [pscustomobject]@{
- Workload = $file.BaseName
- MedianMBps = [Math]::Round($file.Length / 1MB / $median, 1)
- Note = "process wall time, includes startup and file load"
- }
- }
-
- $upstream | Format-Table -AutoSize | Out-String | Tee-Object -FilePath (Join-Path $OutputDirectory "ghostty-bench.txt")
- $upstream | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $OutputDirectory "ghostty-bench.json")
- }
if ($Baseline) {
$previous = Get-Content -LiteralPath $Baseline -Raw | ConvertFrom-Json
@@ -183,13 +145,12 @@ if ($Suite -contains "vt") {
}
# Absolute MB/s drifts with clocks and background load between sessions, so
- # gate on builtin relative to ghostty-vt measured in the same run: Ghostty is
- # the control, and only the engine under our control can regress.
- $oldRatio = $before.PSObject.Properties["builtinVsGhosttyVt"]
- $newRatio = $workload.PSObject.Properties["builtinVsGhosttyVt"]
+ # gate on builtin relative to the clock control measured in the same run.
+ $oldRatio = $before.PSObject.Properties["builtinVsControl"]
+ $newRatio = $workload.PSObject.Properties["builtinVsControl"]
if ($oldRatio -and $newRatio -and $oldRatio.Value -gt 0) {
$change = ($newRatio.Value - $oldRatio.Value) / $oldRatio.Value * 100
- $line = "{0,-14} builtin/ghostty-vt {1:F3} -> {2:F3} ({3:+0.0;-0.0}%)" -f $workload.name, $oldRatio.Value, $newRatio.Value, $change
+ $line = "{0,-14} builtin/control {1:F4} -> {2:F4} ({3:+0.0;-0.0}%)" -f $workload.name, $oldRatio.Value, $newRatio.Value, $change
if ($change -lt -$RegressionPercent) {
$failures.Add("regression: $line")
Write-Host "$line REGRESSION"
diff --git a/scripts/Release-MacOsPackage.ps1 b/scripts/Release-MacOsPackage.ps1
index 08782aa..1b352b3 100644
--- a/scripts/Release-MacOsPackage.ps1
+++ b/scripts/Release-MacOsPackage.ps1
@@ -131,8 +131,7 @@ try {
"$base.dmg",
"$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.EXECUTABLE_NAME)",
"$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.CLI_NAME)",
- "$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.PTY_HOST_NAME)",
- "$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.GHOSTTY_LIBRARY)"
+ "$($metadata.BUNDLE_NAME)/Contents/MacOS/$($metadata.PTY_HOST_NAME)"
)
$manifest = Get-Sha256Manifest -WorkingDirectory $OutputDir -Path $manifestPaths
Set-Content -LiteralPath (Join-Path $OutputDir "$base.sha256") -Value $manifest -Encoding utf8
diff --git a/scripts/Stage-LinuxPackage.sh b/scripts/Stage-LinuxPackage.sh
index 4980f6e..5e6c849 100644
--- a/scripts/Stage-LinuxPackage.sh
+++ b/scripts/Stage-LinuxPackage.sh
@@ -41,10 +41,8 @@ required=(
Devolutions.Terminal
dt
dt-pty-host
- libghostty-vt.so
libSkiaSharp.so
libHarfBuzzSharp.so
- THIRD-PARTY-NOTICES-GHOSTTY.txt
THIRD-PARTY-NOTICES-NOTO-EMOJI.txt
)
for path in "${required[@]}"; do
@@ -78,8 +76,6 @@ install -d "$package_root/usr/bin" "$package_root/usr/share/doc/$PACKAGE_NAME"
ln -s "../../${INSTALL_DIR#/}/Devolutions.Terminal" "$package_root/usr/bin/Devolutions.Terminal"
ln -s "../../${INSTALL_DIR#/}/dt" "$package_root/usr/bin/dt"
install -m 0644 "$repo_root/LICENSE" "$package_root/usr/share/doc/$PACKAGE_NAME/LICENSE"
-install -m 0644 "$publish_dir/THIRD-PARTY-NOTICES-GHOSTTY.txt" \
- "$package_root/usr/share/doc/$PACKAGE_NAME/THIRD-PARTY-NOTICES-GHOSTTY.txt"
find "$package_root" -type d -exec chmod 0755 {} +
find "$package_root" -type f -exec chmod 0644 {} +
diff --git a/scripts/Stage-MacOsApp.ps1 b/scripts/Stage-MacOsApp.ps1
index 29305b5..848bb77 100644
--- a/scripts/Stage-MacOsApp.ps1
+++ b/scripts/Stage-MacOsApp.ps1
@@ -59,10 +59,8 @@ $required = @(
$metadata.EXECUTABLE_NAME,
$metadata.CLI_NAME,
$metadata.PTY_HOST_NAME,
- $metadata.GHOSTTY_LIBRARY,
'libSkiaSharp.dylib',
'libHarfBuzzSharp.dylib',
- 'THIRD-PARTY-NOTICES-GHOSTTY.txt',
'THIRD-PARTY-NOTICES-NOTO-EMOJI.txt'
)
foreach ($path in $required) {
diff --git a/scripts/Stage-MacOsNuGetPayload.ps1 b/scripts/Stage-MacOsNuGetPayload.ps1
index 363318c..dae2ebf 100644
--- a/scripts/Stage-MacOsNuGetPayload.ps1
+++ b/scripts/Stage-MacOsNuGetPayload.ps1
@@ -34,7 +34,7 @@ New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
# Info.plist/resource seals cannot follow an executable out of its bundle.
$binaries = @(
$metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME,
- $metadata.GHOSTTY_LIBRARY, 'libAvaloniaNative.dylib', 'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
+ 'libAvaloniaNative.dylib', 'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
)
foreach ($name in $binaries) {
$source = Join-Path $AppPath "Contents/MacOS/$name"
diff --git a/scripts/Test-ConPtyPublishLayout.ps1 b/scripts/Test-ConPtyPublishLayout.ps1
index d6bb46c..1b98088 100644
--- a/scripts/Test-ConPtyPublishLayout.ps1
+++ b/scripts/Test-ConPtyPublishLayout.ps1
@@ -16,7 +16,7 @@ try {
-
+
@@ -34,7 +34,7 @@ try {
if ($hosts.Count -ne $expectedHosts -or $items.Count -ne $expectedHosts + 3) {
throw "Unexpected publish layout for '$rid': expected $expectedHosts Windows hosts and 3 retained files, got $($hosts.Count) hosts and $($items.Count) files."
}
- foreach ($retained in @('THIRD-PARTY-NOTICES-CONPTY.txt', 'dt', 'libghostty-vt.dylib')) {
+ foreach ($retained in @('THIRD-PARTY-NOTICES-CONPTY.txt', 'dt', 'dt-pty-host')) {
if (@($items | Where-Object { "$($_.Filename)$($_.Extension)" -ceq $retained }).Count -ne 1) {
throw "Publish layout for '$rid' lost $retained."
}
diff --git a/scripts/Test-LinuxArm64Runtime.sh b/scripts/Test-LinuxArm64Runtime.sh
index 6ad7a55..b2b13f4 100644
--- a/scripts/Test-LinuxArm64Runtime.sh
+++ b/scripts/Test-LinuxArm64Runtime.sh
@@ -203,9 +203,7 @@ run_tests Devolutions.Terminal.Cli.Tests \
'FullyQualifiedName~Devolutions.Terminal.Cli.Tests.CliParserTests'
run_tests Devolutions.Terminal.Core.Tests \
'FullyQualifiedName~Devolutions.Terminal.Core.Tests.VtParserTests'
-run_packaged_native_tests Devolutions.Terminal.Ghostty.Tests libghostty-vt.so \
- "$artifact_root$INSTALL_DIR/libghostty-vt.so" \
- 'FullyQualifiedName~Devolutions.Terminal.Ghostty.Tests.GhosttyTerminalEngineTests'
+
run_packaged_native_tests Devolutions.Terminal.Connection.Tests dt-pty-host \
"$artifact_root$INSTALL_DIR/dt-pty-host" \
'FullyQualifiedName~Devolutions.Terminal.Connection.Tests.LinuxPtyConnectionTests'
diff --git a/scripts/Test-LinuxPackage.sh b/scripts/Test-LinuxPackage.sh
index 920a2b0..f0ece9e 100644
--- a/scripts/Test-LinuxPackage.sh
+++ b/scripts/Test-LinuxPackage.sh
@@ -103,7 +103,7 @@ validate_rpm_metadata() {
grep -E -- "^-rwxr-xr-x .* /opt/devolutions-terminal/$executable$" \
<<<"$listing" >/dev/null
done
- for library in libSkiaSharp.so libHarfBuzzSharp.so libghostty-vt.so; do
+ for library in libSkiaSharp.so libHarfBuzzSharp.so; do
grep -E -- "^-rw-r--r-- .* /opt/devolutions-terminal/$library$" \
<<<"$listing" >/dev/null
done
@@ -145,7 +145,7 @@ PY
local listing="$work/appimage-listing"
unsquashfs -lln -o "$(cat "$work/appimage-offset")" "$package" > "$listing"
grep -E -- '^-rwxr-xr-x .* squashfs-root/opt/devolutions-terminal/(Devolutions.Terminal|dt|dt-pty-host)$' "$listing" >/dev/null
- grep -E -- '^-rw-r--r-- .* squashfs-root/opt/devolutions-terminal/lib(SkiaSharp|HarfBuzzSharp|ghostty-vt)\.so$' "$listing" >/dev/null
+ grep -E -- '^-rw-r--r-- .* squashfs-root/opt/devolutions-terminal/lib(SkiaSharp|HarfBuzzSharp)\.so$' "$listing" >/dev/null
unsquashfs -no-progress -o "$(cat "$work/appimage-offset")" -d "$root" "$package" >/dev/null
local runtime_size
runtime_size="$(cat "$work/appimage-offset")"
@@ -281,8 +281,7 @@ validate_root() {
for path in \
"$app/Devolutions.Terminal" "$app/dt" "$app/dt-pty-host" \
- "$app/libghostty-vt.so" "$app/libSkiaSharp.so" "$app/libHarfBuzzSharp.so" \
- "$app/THIRD-PARTY-NOTICES-GHOSTTY.txt" \
+ "$app/libSkiaSharp.so" "$app/libHarfBuzzSharp.so" \
"$root/usr/share/doc/$PACKAGE_NAME/LICENSE" \
"$root/usr/share/doc/$PACKAGE_NAME/inventory.json" \
"$root/usr/share/doc/$PACKAGE_NAME/sbom.spdx.json" \
diff --git a/scripts/Test-MacOsLegalNotices.ps1 b/scripts/Test-MacOsLegalNotices.ps1
index b928a37..a675873 100644
--- a/scripts/Test-MacOsLegalNotices.ps1
+++ b/scripts/Test-MacOsLegalNotices.ps1
@@ -19,7 +19,6 @@ try {
[IO.File]::WriteAllBytes($codePath, [byte[]](0xCF, 0xFA, 0xED, 0xFE))
$codeHash = (Get-FileHash -LiteralPath $codePath).Hash
$notices = @(
- 'THIRD-PARTY-NOTICES-GHOSTTY.txt',
'THIRD-PARTY-NOTICES-NOTO-EMOJI.txt',
'THIRD-PARTY-NOTICES-CONPTY.txt',
'THIRD-PARTY-NOTICES-FUTURE-DEPENDENCY.txt'
diff --git a/scripts/Test-MacOsNuGetPayload.ps1 b/scripts/Test-MacOsNuGetPayload.ps1
index 5a1b283..7ee3dbc 100644
--- a/scripts/Test-MacOsNuGetPayload.ps1
+++ b/scripts/Test-MacOsNuGetPayload.ps1
@@ -21,7 +21,7 @@ if ($RequireDeveloperId) {
}
$binaries = @(
$metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME,
- $metadata.GHOSTTY_LIBRARY, 'libAvaloniaNative.dylib', 'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
+ 'libAvaloniaNative.dylib', 'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
)
foreach ($name in $binaries) {
$path = Join-Path $PayloadDirectory $name
@@ -49,7 +49,7 @@ foreach ($name in $binaries) {
}
}
}
-foreach ($name in @('LICENSE', 'THIRD-PARTY-NOTICES-CONPTY.txt', 'THIRD-PARTY-NOTICES-GHOSTTY.txt', 'THIRD-PARTY-NOTICES-NOTO-EMOJI.txt')) {
+foreach ($name in @('LICENSE', 'THIRD-PARTY-NOTICES-CONPTY.txt', 'THIRD-PARTY-NOTICES-NOTO-EMOJI.txt')) {
if (-not (Test-Path -LiteralPath (Join-Path $PayloadDirectory $name) -PathType Leaf)) {
throw "NuGet payload is missing $name."
}
diff --git a/scripts/Test-MacOsNuGetSigning.ps1 b/scripts/Test-MacOsNuGetSigning.ps1
index b96c867..7b96902 100644
--- a/scripts/Test-MacOsNuGetSigning.ps1
+++ b/scripts/Test-MacOsNuGetSigning.ps1
@@ -22,7 +22,7 @@ try {
Invoke-Native -FilePath clang -ArgumentList '-arch', (Get-MacOsExpectedArch -Rid $rid), $fixtureSource, '-o', $fixtureBinary
foreach ($name in @(
$metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME,
- $metadata.GHOSTTY_LIBRARY, 'libAvaloniaNative.dylib', 'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
+ 'libAvaloniaNative.dylib', 'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
)) {
Invoke-Native -FilePath cp -ArgumentList $fixtureBinary, (Join-Path $code $name)
}
@@ -34,7 +34,7 @@ try {
CFBundlePackageTypeAPPL
"@ | Set-Content (Join-Path $app 'Contents/Info.plist') -Encoding utf8
- foreach ($name in @('LICENSE', 'THIRD-PARTY-NOTICES-CONPTY.txt', 'THIRD-PARTY-NOTICES-GHOSTTY.txt', 'THIRD-PARTY-NOTICES-NOTO-EMOJI.txt')) {
+ foreach ($name in @('LICENSE', 'THIRD-PARTY-NOTICES-CONPTY.txt', 'THIRD-PARTY-NOTICES-NOTO-EMOJI.txt')) {
Set-Content (Join-Path $resources $name) "fixture license $name" -NoNewline
}
& (Join-Path $PSScriptRoot 'Sign-MacOsPackage.ps1') $app '-'
diff --git a/scripts/Test-MacOsPackage.ps1 b/scripts/Test-MacOsPackage.ps1
index 629caf7..7f942da 100644
--- a/scripts/Test-MacOsPackage.ps1
+++ b/scripts/Test-MacOsPackage.ps1
@@ -61,10 +61,9 @@ function Test-MacOsAppBundle {
(Join-Path $macosDir $metadata.EXECUTABLE_NAME),
(Join-Path $macosDir $metadata.CLI_NAME),
(Join-Path $macosDir $metadata.PTY_HOST_NAME),
- (Join-Path $macosDir $metadata.GHOSTTY_LIBRARY),
(Join-Path $macosDir 'libSkiaSharp.dylib'),
(Join-Path $macosDir 'libHarfBuzzSharp.dylib'),
- (Join-Path $App 'Contents/Resources/THIRD-PARTY-NOTICES-GHOSTTY.txt'),
+
(Join-Path $App 'Contents/Resources/THIRD-PARTY-NOTICES-NOTO-EMOJI.txt'),
(Join-Path $App 'Contents/Resources/LICENSE'),
$plist,
@@ -136,7 +135,7 @@ function Test-MacOsAppBundle {
}
foreach ($binary in @(
- $metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME, $metadata.GHOSTTY_LIBRARY,
+ $metadata.EXECUTABLE_NAME, $metadata.CLI_NAME, $metadata.PTY_HOST_NAME,
'libSkiaSharp.dylib', 'libHarfBuzzSharp.dylib'
)) {
$binaryPath = Join-Path $macosDir $binary
diff --git a/scripts/Test-MacOsPackagingMetadata.ps1 b/scripts/Test-MacOsPackagingMetadata.ps1
index 228169d..f7b154a 100644
--- a/scripts/Test-MacOsPackagingMetadata.ps1
+++ b/scripts/Test-MacOsPackagingMetadata.ps1
@@ -63,7 +63,6 @@ Assert-Equal -Actual $metadata.APPLE_TEAM_ID -Expected 'N592S9ASDB' -Name 'APPLE
Assert-Equal -Actual $metadata.EXECUTABLE_NAME -Expected 'Devolutions.Terminal' -Name 'EXECUTABLE_NAME'
Assert-Equal -Actual $metadata.CLI_NAME -Expected 'dt' -Name 'CLI_NAME'
Assert-Equal -Actual $metadata.PTY_HOST_NAME -Expected 'dt-pty-host' -Name 'PTY_HOST_NAME'
-Assert-Equal -Actual $metadata.GHOSTTY_LIBRARY -Expected 'libghostty-vt.dylib' -Name 'GHOSTTY_LIBRARY'
Assert-Equal -Actual $metadata.ICON_NAME -Expected 'DevolutionsTerminal' -Name 'ICON_NAME'
Assert-Equal -Actual $metadata.MACOS_DEPLOYMENT_TARGET -Expected '13.0' -Name 'MACOS_DEPLOYMENT_TARGET'
Assert-Equal -Actual $metadata.LICENSE_ID -Expected 'MIT AND OFL-1.1' -Name 'LICENSE_ID'
diff --git a/scripts/Test-MacOsRuntime.ps1 b/scripts/Test-MacOsRuntime.ps1
index 8832bf8..4869593 100644
--- a/scripts/Test-MacOsRuntime.ps1
+++ b/scripts/Test-MacOsRuntime.ps1
@@ -4,7 +4,7 @@
.SYNOPSIS
Runs native, non-UI runtime gates for the host macOS architecture: a
packaged `dt`/`dt-pty-host` smoke test plus targeted unit tests against
- the packaged Ghostty and PTY-host native libraries.
+ the packaged PTY host.
.PARAMETER PackageDir
Directory containing the packaged zip(s) (defaults to artifacts/packages).
@@ -171,9 +171,7 @@ try {
-Filter 'FullyQualifiedName~Devolutions.Terminal.Cli.Tests.CliParserTests'
Invoke-DotnetTest -Project 'Devolutions.Terminal.Core.Tests' `
-Filter 'FullyQualifiedName~Devolutions.Terminal.Core.Tests.VtParserTests'
- Invoke-PackagedNativeTest -Project 'Devolutions.Terminal.Ghostty.Tests' -NativeName 'libghostty-vt.dylib' `
- -PackagedNative (Join-Path $macosDir $metadata.GHOSTTY_LIBRARY) `
- -Filter 'FullyQualifiedName~Devolutions.Terminal.Ghostty.Tests.GhosttyTerminalEngineTests'
+
Invoke-PackagedNativeTest -Project 'Devolutions.Terminal.Connection.Tests' -NativeName 'dt-pty-host' `
-PackagedNative (Join-Path $macosDir $metadata.PTY_HOST_NAME) `
-Filter 'FullyQualifiedName~Devolutions.Terminal.Connection.Tests.LinuxPtyConnectionTests'
diff --git a/src/Devolutions.Terminal.App/Connections/TerminalEngineFactory.cs b/src/Devolutions.Terminal.App/Connections/TerminalEngineFactory.cs
index 0d19fc8..3ac9554 100644
--- a/src/Devolutions.Terminal.App/Connections/TerminalEngineFactory.cs
+++ b/src/Devolutions.Terminal.App/Connections/TerminalEngineFactory.cs
@@ -1,5 +1,4 @@
using Devolutions.Terminal.Core;
-using Devolutions.Terminal.Ghostty;
using Devolutions.Terminal.Settings;
namespace Devolutions.Terminal.App.Connections;
@@ -10,18 +9,6 @@ public static ITerminalEngine Create(AppSettings settings, ProfileSettings profi
{
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(profile);
- if (OperatingSystem.IsBrowser())
- {
- return new TerminalEngine(historySize: profile.HistorySize);
- }
-
- return (profile.TerminalEngine ??
- settings.ProfileDefaults.TerminalEngine ??
- settings.TerminalEngine) switch
- {
- TerminalEngineKind.Ghostty => new GhosttyTerminalEngine(
- historySize: profile.HistorySize),
- _ => new TerminalEngine(historySize: profile.HistorySize),
- };
+ return new TerminalEngine(historySize: profile.HistorySize);
}
}
diff --git a/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj b/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj
index a58d617..3cea5df 100644
--- a/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj
+++ b/src/Devolutions.Terminal.App/Devolutions.Terminal.App.csproj
@@ -27,7 +27,6 @@
-
diff --git a/src/Devolutions.Terminal.App/Platform/PlatformLauncher.cs b/src/Devolutions.Terminal.App/Platform/PlatformLauncher.cs
index dd7c8a7..6626a8d 100644
--- a/src/Devolutions.Terminal.App/Platform/PlatformLauncher.cs
+++ b/src/Devolutions.Terminal.App/Platform/PlatformLauncher.cs
@@ -143,8 +143,7 @@ public string GetCapabilityReport()
"Open URI/file/directory: available through open(1)\n" +
"System notifications: available through osascript display notification\n" +
"Global summon hotkeys: unsupported; use the broker or dt -w\n" +
- "Unix PTY: dt-pty-host (forkpty)\n" +
- GetMacOsGhosttyCapability(),
+ "Unix PTY: dt-pty-host (forkpty)",
_ => "Desktop platform: unsupported\nOpen and notification integrations are unavailable.",
};
}
@@ -161,13 +160,6 @@ private string GetWindowsCapabilityReport()
$"Default terminal: {defaultTerminal.Status} ({defaultTerminal.Diagnostic})";
}
- private static string GetMacOsGhosttyCapability()
- {
- var library = Path.Combine(AppContext.BaseDirectory, "libghostty-vt.dylib");
- return File.Exists(library)
- ? "Ghostty engine: available (libghostty-vt.dylib)"
- : "Ghostty engine: not restored for this build";
- }
private static string Availability(
ShellIntegrationResult result,
diff --git a/src/Devolutions.Terminal.Connection/BrowserShellConnection.cs b/src/Devolutions.Terminal.Connection/BrowserShellConnection.cs
index c2107b6..9d62fe4 100644
--- a/src/Devolutions.Terminal.Connection/BrowserShellConnection.cs
+++ b/src/Devolutions.Terminal.Connection/BrowserShellConnection.cs
@@ -27,8 +27,8 @@ public sealed class BrowserShellConnection : IRestartableTerminalConnection
{
["README.md"] =
"Devolutions Terminal in the browser.\n" +
- "This session is an in-process shell. ConPTY, Unix PTY, Ghostty,\n" +
- "and the desktop window chrome are not available under WebAssembly.\n",
+ "This session is an in-process shell. ConPTY, Unix PTY, and the\n" +
+ "desktop window chrome are not available under WebAssembly.\n",
["help.txt"] =
"help, echo, cat, ls, clear, date, uname, whoami, env, history, color, about, exit\n",
};
@@ -249,7 +249,7 @@ private void StartCore(TerminalLaunchOptions options)
Emit(
"\u001b[1;36mDevolutions Terminal\u001b[0m \u001b[90m· browser wasm\u001b[0m\r\n" +
- "\u001b[90mBuilt-in VT engine. Local PTY, Ghostty, and desktop chrome are unavailable.\u001b[0m\r\n" +
+ "\u001b[90mBuilt-in VT engine. Local PTY and desktop chrome are unavailable.\u001b[0m\r\n" +
"\r\nType \u001b[1mhelp\u001b[0m for commands.\r\n\r\n");
Emit(Prompt);
}
diff --git a/src/Devolutions.Terminal.Control/TerminalSearchSession.cs b/src/Devolutions.Terminal.Control/TerminalSearchSession.cs
index 3b51f7e..9e9b578 100644
--- a/src/Devolutions.Terminal.Control/TerminalSearchSession.cs
+++ b/src/Devolutions.Terminal.Control/TerminalSearchSession.cs
@@ -174,7 +174,7 @@ private int FindAnchor(SearchAnchor anchor, int fallbackIndex)
return 0;
}
- // Ghostty rebuilds projected rows (and their IDs) for each snapshot.
+ // Only the builtin engine keeps logical-line IDs stable across snapshots.
if (_engine is not TerminalEngine || anchor.LogicalLineId == 0)
{
return FindTextAnchor(anchor, fallbackIndex);
diff --git a/src/Devolutions.Terminal.Ghostty/Devolutions.Terminal.Ghostty.csproj b/src/Devolutions.Terminal.Ghostty/Devolutions.Terminal.Ghostty.csproj
deleted file mode 100644
index b5a1473..0000000
--- a/src/Devolutions.Terminal.Ghostty/Devolutions.Terminal.Ghostty.csproj
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- true
-
-
-
-
-
-
diff --git a/src/Devolutions.Terminal.Ghostty/GhosttyAbi.cs b/src/Devolutions.Terminal.Ghostty/GhosttyAbi.cs
deleted file mode 100644
index 3491080..0000000
--- a/src/Devolutions.Terminal.Ghostty/GhosttyAbi.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Runtime.InteropServices;
-using System.Text.Json;
-
-namespace Devolutions.Terminal.Ghostty;
-
-public static class GhosttyAbi
-{
- public const string Commit = "3c1ef5b32fc5ea6b93d28493fabf193f595139cf";
- public const int SchemaVersion = 1;
-
- private static readonly Lazy ValidatedManifest = new(ValidateCore);
-
- public static string TypeManifest => ValidatedManifest.Value;
-
- public static string NativeLibraryFileName => OperatingSystem.IsWindows()
- ? "ghostty-vt.dll"
- : OperatingSystem.IsMacOS()
- ? "libghostty-vt.dylib"
- : "libghostty-vt.so";
-
- public static bool IsNativeLibraryPresent =>
- File.Exists(Path.Combine(AppContext.BaseDirectory, NativeLibraryFileName));
-
- public static void Validate() => _ = ValidatedManifest.Value;
-
- private static string ValidateCore()
- {
- if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS())
- {
- throw new PlatformNotSupportedException(
- $"The Ghostty engine does not support {RuntimeInformation.OSDescription} {RuntimeInformation.ProcessArchitecture}.");
- }
-
- var libraryPath = Path.Combine(AppContext.BaseDirectory, NativeLibraryFileName);
- if (!File.Exists(libraryPath))
- {
- throw new FileNotFoundException(
- $"libghostty-vt was not restored for this RID. Build it with native/Restore-NativeLibraries.ps1 (commit {Commit}).",
- libraryPath);
- }
-
- var pointer = GhosttyNative.TypeJson();
- var manifest = Marshal.PtrToStringUTF8(pointer)
- ?? throw new InvalidOperationException("libghostty-vt returned no ABI type manifest.");
- using var document = JsonDocument.Parse(manifest);
- var root = document.RootElement;
- if (root.GetProperty("schema").GetInt32() != SchemaVersion)
- {
- throw new InvalidOperationException(
- $"Unsupported libghostty-vt ABI schema. Expected schema {SchemaVersion} from {Commit}.");
- }
-
- var abi = root.GetProperty("abi");
- var expectedTarget = RuntimeInformation.ProcessArchitecture == Architecture.Arm64
- ? "aarch64"
- : "x86_64";
- var expectedOs = OperatingSystem.IsWindows()
- ? "windows"
- : OperatingSystem.IsMacOS()
- ? "macos"
- : "linux";
- var expectedEnvironment = OperatingSystem.IsWindows()
- ? "msvc"
- : OperatingSystem.IsMacOS()
- ? "none"
- : "gnu";
- if (abi.GetProperty("target").GetString() != expectedTarget ||
- abi.GetProperty("os").GetString() != expectedOs ||
- abi.GetProperty("environment").GetString() != expectedEnvironment ||
- abi.GetProperty("pointer_size").GetInt32() != IntPtr.Size)
- {
- throw new InvalidOperationException("ghostty-vt.dll ABI target does not match this process.");
- }
-
- var types = root.GetProperty("types");
- ValidateSize(types, "GhosttyStyle", 72);
- ValidateSize(types, "GhosttyRenderStateCursor", 24);
- ValidateSize(types, "GhosttyClipboardWrite", 72);
- ValidateSize(types, "GhosttyPoint", 24);
- ValidateSize(types, "GhosttyGridRef", 24);
- ValidateEnum(types, "GhosttyTerminalOption", "SCROLLBACK_MAX_LINES", 28);
- ValidateEnum(types, "GhosttyTerminalData", "MODE", 37);
- ValidateEnum(types, "GhosttyRenderStateData", "CURSOR", 18);
- ValidateEnum(types, "GhosttyRenderStateRowCellsData", "GRAPHEMES_UTF8", 9);
- return manifest;
- }
-
- private static void ValidateSize(JsonElement types, string name, int expected)
- {
- if (types.GetProperty(name).GetProperty("size").GetInt32() != expected)
- {
- throw new InvalidOperationException($"Unexpected {name} ABI size.");
- }
- }
-
- private static void ValidateEnum(
- JsonElement types,
- string type,
- string name,
- int expected)
- {
- if (types.GetProperty(type).GetProperty("values").GetProperty(name).GetInt32() != expected)
- {
- throw new InvalidOperationException($"Unexpected {type}.{name} ABI value.");
- }
- }
-}
diff --git a/src/Devolutions.Terminal.Ghostty/GhosttyNative.cs b/src/Devolutions.Terminal.Ghostty/GhosttyNative.cs
deleted file mode 100644
index a55f906..0000000
--- a/src/Devolutions.Terminal.Ghostty/GhosttyNative.cs
+++ /dev/null
@@ -1,253 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using Microsoft.Win32.SafeHandles;
-
-namespace Devolutions.Terminal.Ghostty;
-
-internal static unsafe partial class GhosttyNative
-{
- private const string LibraryName = "ghostty-vt";
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_new")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult TerminalNew(
- nint allocator,
- out nint terminal,
- ushort columns,
- ushort rows);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void TerminalFree(nint terminal);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_vt_write")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void TerminalWrite(
- nint terminal,
- byte* bytes,
- nuint length);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_resize")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult TerminalResize(
- nint terminal,
- ushort columns,
- ushort rows,
- uint cellWidth,
- uint cellHeight);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_reset")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void TerminalReset(nint terminal);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_get")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult TerminalGet(
- nint terminal,
- int data,
- void* output);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_set")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult TerminalSet(
- nint terminal,
- int option,
- void* value);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_scroll_viewport")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void TerminalScrollViewport(
- nint terminal,
- GhosttyScrollViewport viewport);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_grid_ref")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult TerminalGridRef(
- nint terminal,
- GhosttyPoint point,
- GhosttyGridRef* gridRef);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_terminal_grid_ref_track")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult TerminalGridRefTrack(
- nint terminal,
- GhosttyPoint point,
- out nint trackedGridRef);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_tracked_grid_ref_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void TrackedGridRefFree(nint trackedGridRef);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_tracked_grid_ref_has_value")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- [return: MarshalAs(UnmanagedType.I1)]
- internal static partial bool TrackedGridRefHasValue(nint trackedGridRef);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_grid_ref_hyperlink_uri")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult GridRefHyperlinkUri(
- GhosttyGridRef* gridRef,
- byte* buffer,
- nuint bufferLength,
- nuint* written);
-
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_new")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RenderStateNew(
- nint allocator,
- out nint state);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void RenderStateFree(nint state);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_update")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RenderStateUpdate(
- nint state,
- nint terminal);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_clean")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RenderStateClean(nint state);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_get")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RenderStateGet(
- nint state,
- int data,
- void* output);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_iterator_new")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RowIteratorNew(
- nint allocator,
- out nint iterator);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_iterator_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void RowIteratorFree(nint iterator);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_iterator_next")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- [return: MarshalAs(UnmanagedType.I1)]
- internal static partial bool RowIteratorNext(nint iterator);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_get")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RowGet(
- nint iterator,
- int data,
- void* output);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_cells_new")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RowCellsNew(
- nint allocator,
- out nint cells);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_cells_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial void RowCellsFree(nint cells);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_cells_next")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- [return: MarshalAs(UnmanagedType.I1)]
- internal static partial bool RowCellsNext(nint cells);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_render_state_row_cells_get")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RowCellsGet(
- nint cells,
- int data,
- void* output);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_cell_get")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult CellGet(
- ulong cell,
- int data,
- void* output);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_row_get")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial GhosttyResult RawRowGet(
- ulong row,
- int data,
- void* output);
-
- [LibraryImport(LibraryName, EntryPoint = "ghostty_type_json")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- internal static partial nint TypeJson();
-}
-
-internal sealed class SafeGhosttyTerminalHandle : SafeHandleZeroOrMinusOneIsInvalid
-{
- internal SafeGhosttyTerminalHandle(nint value) : base(true)
- {
- SetHandle(value);
- }
-
- protected override bool ReleaseHandle()
- {
- GhosttyNative.TerminalFree(handle);
- return true;
- }
-}
-
-internal sealed class SafeGhosttyRenderStateHandle : SafeHandleZeroOrMinusOneIsInvalid
-{
- internal SafeGhosttyRenderStateHandle(nint value) : base(true)
- {
- SetHandle(value);
- }
-
- protected override bool ReleaseHandle()
- {
- GhosttyNative.RenderStateFree(handle);
- return true;
- }
-}
-
-internal sealed class SafeGhosttyRowIteratorHandle : SafeHandleZeroOrMinusOneIsInvalid
-{
- internal SafeGhosttyRowIteratorHandle(nint value) : base(true)
- {
- SetHandle(value);
- }
-
- protected override bool ReleaseHandle()
- {
- GhosttyNative.RowIteratorFree(handle);
- return true;
- }
-}
-
-internal sealed class SafeGhosttyRowCellsHandle : SafeHandleZeroOrMinusOneIsInvalid
-{
- internal SafeGhosttyRowCellsHandle(nint value) : base(true)
- {
- SetHandle(value);
- }
-
- protected override bool ReleaseHandle()
- {
- GhosttyNative.RowCellsFree(handle);
- return true;
- }
-}
-
-internal sealed class SafeGhosttyTrackedGridRefHandle : SafeHandleZeroOrMinusOneIsInvalid
-{
- internal SafeGhosttyTrackedGridRefHandle(nint value) : base(true)
- {
- SetHandle(value);
- }
-
- protected override bool ReleaseHandle()
- {
- GhosttyNative.TrackedGridRefFree(handle);
- return true;
- }
-}
diff --git a/src/Devolutions.Terminal.Ghostty/GhosttyNativeTypes.cs b/src/Devolutions.Terminal.Ghostty/GhosttyNativeTypes.cs
deleted file mode 100644
index de9f996..0000000
--- a/src/Devolutions.Terminal.Ghostty/GhosttyNativeTypes.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-using System.Runtime.InteropServices;
-
-namespace Devolutions.Terminal.Ghostty;
-
-internal enum GhosttyResult
-{
- Success = 0,
- OutOfMemory = -1,
- InvalidValue = -2,
- OutOfSpace = -3,
- NoValue = -4,
- IoError = -5,
- LimitExceeded = -6,
- Rejected = -7,
-}
-
-[StructLayout(LayoutKind.Sequential, Pack = 1)]
-internal struct GhosttyColorRgb
-{
- public byte R;
- public byte G;
- public byte B;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 16)]
-internal struct GhosttyStyleColor
-{
- [FieldOffset(0)]
- public int Tag;
-
- [FieldOffset(8)]
- public byte Palette;
-
- [FieldOffset(8)]
- public GhosttyColorRgb Rgb;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 72)]
-internal struct GhosttyStyle
-{
- [FieldOffset(0)]
- public nuint Size;
-
- [FieldOffset(8)]
- public GhosttyStyleColor Foreground;
-
- [FieldOffset(24)]
- public GhosttyStyleColor Background;
-
- [FieldOffset(40)]
- public GhosttyStyleColor UnderlineColor;
-
- [FieldOffset(56)]
- public byte Bold;
-
- [FieldOffset(57)]
- public byte Italic;
-
- [FieldOffset(58)]
- public byte Faint;
-
- [FieldOffset(59)]
- public byte Blink;
-
- [FieldOffset(60)]
- public byte Inverse;
-
- [FieldOffset(61)]
- public byte Invisible;
-
- [FieldOffset(62)]
- public byte Strikethrough;
-
- [FieldOffset(63)]
- public byte Overline;
-
- [FieldOffset(64)]
- public int Underline;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 24)]
-internal struct GhosttyRenderCursor
-{
- [FieldOffset(0)]
- public nuint Size;
-
- [FieldOffset(8)]
- public byte ViewportHasValue;
-
- [FieldOffset(10)]
- public ushort ViewportX;
-
- [FieldOffset(12)]
- public ushort ViewportY;
-
- [FieldOffset(14)]
- public byte WideTail;
-
- [FieldOffset(15)]
- public byte Visible;
-
- [FieldOffset(16)]
- public byte Blinking;
-
- [FieldOffset(17)]
- public byte PasswordInput;
-
- [FieldOffset(20)]
- public int VisualStyle;
-}
-
-[StructLayout(LayoutKind.Sequential)]
-internal struct GhosttyString
-{
- public nint Pointer;
- public nuint Length;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 4)]
-internal struct GhosttyModeConfig
-{
- [FieldOffset(0)]
- public ushort Mode;
-
- [FieldOffset(2)]
- public byte Value;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 24)]
-internal struct GhosttyScrollViewport
-{
- [FieldOffset(0)]
- public int Tag;
-
- [FieldOffset(8)]
- public nuint Row;
-}
-
-[StructLayout(LayoutKind.Sequential)]
-internal struct GhosttyScrollbar
-{
- public ulong Total;
- public ulong Offset;
- public ulong Length;
-}
-
-[StructLayout(LayoutKind.Sequential)]
-internal struct GhosttyClipboardContent
-{
- public GhosttyString Mime;
- public GhosttyString Data;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 72)]
-internal unsafe struct GhosttyClipboardWrite
-{
- [FieldOffset(0)]
- public nuint Size;
-
- [FieldOffset(16)]
- public GhosttyClipboardContent* Contents;
-
- [FieldOffset(24)]
- public nuint ContentsLength;
-
- [FieldOffset(64)]
- public nint Reply;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 16)]
-internal struct GhosttyClipboardWriteReply
-{
- [FieldOffset(0)]
- public nuint Size;
-
- [FieldOffset(8)]
- public int Result;
-
- [FieldOffset(12)]
- public byte Remember;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 40)]
-internal struct GhosttyDesktopNotification
-{
- [FieldOffset(0)]
- public nuint Size;
-
- [FieldOffset(8)]
- public GhosttyString Title;
-
- [FieldOffset(24)]
- public GhosttyString Body;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 24)]
-internal struct GhosttyPoint
-{
- [FieldOffset(0)]
- public int Tag;
-
- [FieldOffset(8)]
- public ushort X;
-
- [FieldOffset(12)]
- public uint Y;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 24)]
-internal struct GhosttyGridRef
-{
- [FieldOffset(0)]
- public nuint Size;
-
- [FieldOffset(8)]
- public nint Node;
-
- [FieldOffset(16)]
- public ushort X;
-
- [FieldOffset(18)]
- public ushort Y;
-}
-
-[StructLayout(LayoutKind.Sequential)]
-internal struct GhosttySizeReport
-{
- public ushort Rows;
- public ushort Columns;
- public uint CellWidth;
- public uint CellHeight;
-}
-
-internal sealed class GhosttyException(string operation, GhosttyResult result)
- : InvalidOperationException($"{operation} failed with Ghostty result {result} ({(int)result}).")
-{
- public GhosttyResult Result { get; } = result;
-}
diff --git a/src/Devolutions.Terminal.Ghostty/GhosttyTerminalEngine.cs b/src/Devolutions.Terminal.Ghostty/GhosttyTerminalEngine.cs
deleted file mode 100644
index 3e3c926..0000000
--- a/src/Devolutions.Terminal.Ghostty/GhosttyTerminalEngine.cs
+++ /dev/null
@@ -1,1609 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text;
-using Devolutions.Terminal.Core;
-
-namespace Devolutions.Terminal.Ghostty;
-
-public sealed unsafe class GhosttyTerminalEngine : ITerminalEngine
-{
- private const int TerminalOptionUserData = 0;
- private const int TerminalOptionWritePty = 1;
- private const int TerminalOptionBell = 2;
- private const int TerminalOptionTitleChanged = 5;
- private const int TerminalOptionSize = 6;
- private const int TerminalOptionPwdChanged = 25;
- private const int TerminalOptionClipboardWrite = 26;
- private const int TerminalOptionDesktopNotification = 29;
-
- private const int TerminalDataActiveScreen = 6;
- private const int TerminalDataTitle = 12;
- private const int TerminalDataPwd = 13;
- private const int TerminalDataMode = 37;
-
- private const int RenderDataCols = 1;
- private const int RenderDataRows = 2;
- private const int RenderDataDirty = 3;
- private const int RenderDataRowIterator = 4;
- private const int RenderDataCursor = 18;
- private const int RenderStateDirtyFull = 2;
- private const int RenderRowDataDirty = 1;
- private const int RenderRowDataRaw = 2;
- private const int RenderRowDataCells = 3;
- private const int RenderCellDataStyle = 2;
- private const int RenderCellDataRaw = 1;
- private const int RenderCellDataGraphemeLength = 3;
- private const int RenderCellDataGraphemeBuffer = 4;
- private const int RenderCellDataBackground = 5;
- private const int RenderCellDataForeground = 6;
-
- private readonly object _gate = new();
- private readonly SafeGhosttyTerminalHandle _terminal;
- private readonly SafeGhosttyRenderStateHandle _renderState;
- private readonly SafeGhosttyRowIteratorHandle _rowIterator;
- private readonly SafeGhosttyRowCellsHandle _rowCells;
- private readonly GCHandle _selfHandle;
- private ColorScheme _scheme = ColorScheme.Campbell;
- private bool _disposed;
- private bool _allowClipboardWrite;
- private bool _allowNotifications;
- private bool _allowKittyKeyboard = true;
- private KittyKeyboardFlags _kittyKeyboardFlags;
- private readonly Stack _kittyKeyboardStack = new(16);
- private string _title = "Devolutions Terminal";
- private string? _workingDirectory;
- private GhosttyRenderCursor _cursor;
- private readonly TextBuffer _buffer;
- // Feeds only mark the projection stale; readers of the projected buffer or
- // cursor rebuild it on demand, once per frame rather than once per PTY read.
- private bool _projectionStale;
- // The last projection of each viewport row, reused while Ghostty's render
- // state reports the row clean.
- private (Cell[] Cells, int? PromptStart)?[] _projectedRows = [];
- private uint[] _codepointBuffer = new uint[16];
- private byte[] _hyperlinkBuffer = new byte[256];
- private Exception? _callbackException;
- private SafeGhosttyTrackedGridRefHandle? _historyAnchor;
- private readonly int _historyLimit;
- private uint _cellWidthPixels = 1;
- private uint _cellHeightPixels = 1;
- private readonly List _imageProbe = new(16);
- private ImageProbeState _imageProbeState;
- private bool _dcsImageCandidate;
- private bool _ansiMode = true;
- private int _modifyOtherKeys;
- private KeyboardModeScanState _keyboardModeScan;
- private bool _kittyImageReported;
- private byte _keyboardCsiPrivate;
- private int _keyboardCsiValue;
- private bool _keyboardCsiHasValue;
- private readonly int[] _keyboardCsiParams = new int[8];
- private int _keyboardCsiCount;
-
- private enum ImageProbeState : byte
- {
- Ground,
- Escape,
- DcsHeader,
- DcsBody,
- DcsEscape,
- Osc,
- OscEscape,
- OscIgnored,
- OscIgnoredEscape,
- Apc,
- ApcIgnored,
- ApcIgnoredEscape,
- }
-
- private enum KeyboardModeScanState : byte
- {
- Ground,
- Escape,
- Csi,
- }
-
- public GhosttyTerminalEngine(int columns = 120, int rows = 30, int historySize = 9001)
- {
- GhosttyAbi.Validate();
- _historyLimit = Math.Max(0, historySize);
- _buffer = new TextBuffer(columns, rows, historySize, hasHistory: true);
- nint terminal = 0;
- nint renderState = 0;
- nint rowIterator = 0;
- nint rowCells = 0;
- try
- {
- ThrowIfFailed(
- GhosttyNative.TerminalNew(
- 0,
- out terminal,
- checked((ushort)columns),
- checked((ushort)rows)),
- "ghostty_terminal_new");
- ThrowIfFailed(GhosttyNative.RenderStateNew(0, out renderState), "ghostty_render_state_new");
- ThrowIfFailed(GhosttyNative.RowIteratorNew(0, out rowIterator), "ghostty_render_state_row_iterator_new");
- ThrowIfFailed(GhosttyNative.RowCellsNew(0, out rowCells), "ghostty_render_state_row_cells_new");
- }
- catch
- {
- if (rowCells != 0) GhosttyNative.RowCellsFree(rowCells);
- if (rowIterator != 0) GhosttyNative.RowIteratorFree(rowIterator);
- if (renderState != 0) GhosttyNative.RenderStateFree(renderState);
- if (terminal != 0) GhosttyNative.TerminalFree(terminal);
- throw;
- }
-
- _terminal = new SafeGhosttyTerminalHandle(terminal);
- _renderState = new SafeGhosttyRenderStateHandle(renderState);
- _rowIterator = new SafeGhosttyRowIteratorHandle(rowIterator);
- _rowCells = new SafeGhosttyRowCellsHandle(rowCells);
- _selfHandle = GCHandle.Alloc(this);
- try
- {
- RegisterEffects();
- var scrollbackLines = (nuint)Math.Max(0, historySize);
- if (historySize == 0)
- {
- var scrollbackBytes = (nuint)0;
- ThrowIfFailed(
- GhosttyNative.TerminalSet(
- _terminal.DangerousGetHandle(),
- 27,
- &scrollbackBytes),
- "disable scrollback bytes");
- }
- else
- {
- ThrowIfFailed(
- GhosttyNative.TerminalSet(
- _terminal.DangerousGetHandle(),
- 27,
- null),
- "remove scrollback byte limit");
- }
-
- ThrowIfFailed(
- GhosttyNative.TerminalSet(
- _terminal.DangerousGetHandle(),
- 28,
- &scrollbackLines),
- "set scrollback line limit");
- ApplyScheme();
- SynchronizeProjection();
- ResetHistoryAnchor();
- }
- catch
- {
- Dispose();
- throw;
- }
- }
-
- public TextBuffer Buffer
- {
- get
- {
- lock (_gate)
- {
- EnsureProjection();
- }
-
- return _buffer;
- }
- }
-
- private GhosttyRenderCursor ProjectedCursor
- {
- get
- {
- lock (_gate)
- {
- EnsureProjection();
- return _cursor;
- }
- }
- }
-
- public ColorScheme Scheme
- {
- get => _scheme;
- set
- {
- ArgumentNullException.ThrowIfNull(value);
- lock (_gate)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- _scheme = value;
- ApplyScheme();
- SynchronizeProjection();
- }
-
- RaiseInvalidated();
- }
- }
-
- public string Title => _title;
- public TerminalEngineCapabilities Capabilities => TerminalEngineCapabilities.Vt52Keyboard;
- public string? WorkingDirectory => _workingDirectory;
- public bool AlternateBufferActive => QueryInt(TerminalDataActiveScreen) == 1;
- public bool CursorVisible => ProjectedCursor is { ViewportHasValue: not 0, Visible: not 0 };
- public bool CursorBlinking => ProjectedCursor.Blinking != 0;
- public int CursorStyle =>
- ProjectedCursor.VisualStyle is var style and >= 1 and <= 6 ? style : 0;
- public bool ApplicationCursorKeys => QueryMode(1);
- public bool BracketedPaste => QueryMode(2004);
- public bool MouseTracking => QueryMode(9) || QueryMode(1000) || QueryMode(1002) || QueryMode(1003);
- public TerminalMouseTrackingMode MouseTrackingMode =>
- QueryMode(1003) ? TerminalMouseTrackingMode.AllMotion :
- QueryMode(1002) ? TerminalMouseTrackingMode.ButtonEvent :
- QueryMode(1000) ? TerminalMouseTrackingMode.Button :
- QueryMode(9) ? TerminalMouseTrackingMode.Button :
- TerminalMouseTrackingMode.None;
- public bool SgrMouse => QueryMode(1006);
- public bool FocusTracking => QueryMode(1004);
- public bool AutoWrap => QueryMode(7);
- public bool InsertMode => QueryMode(4, ansi: true);
- public bool ReverseVideo => QueryMode(5);
- public TerminalInputMode InputMode => new(
- AnsiMode: _ansiMode,
- ApplicationCursorKeys,
- ApplicationKeypad: QueryMode(66),
- KittyFlags: _allowKittyKeyboard ? _kittyKeyboardFlags : KittyKeyboardFlags.None,
- ModifyOtherKeys: _modifyOtherKeys,
- Win32InputMode: false);
- public int Columns => Buffer.Columns;
- public int Rows => Buffer.Rows;
- public int CursorX => Buffer.CursorX;
- public int CursorY => Buffer.CursorY;
- public uint PixelWidth => QueryUInt32(16);
- public uint PixelHeight => QueryUInt32(17);
- public int HistoryCount => ReadScrollbar().HistoryCount;
- public int ScrollOffset => ReadScrollbar().ScrollOffset;
-
- public event EventHandler? Invalidated;
- public event EventHandler? TitleChanged;
- public event EventHandler? WorkingDirectoryChanged;
- public event EventHandler? ShellIntegrationChanged;
- public event EventHandler? ClipboardWriteRequested;
- public event EventHandler? NotificationRequested;
- public event EventHandler? Diagnostic;
- public event EventHandler? Bell;
- public event EventHandler? ResponseReady;
-
- public void Feed(ReadOnlySpan data)
- {
- lock (_gate)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- ProbeUnsupportedImages(data);
- TrackHostKeyboardModes(data);
- var trackedHistory = _historyAnchor is { IsInvalid: false, IsClosed: false };
- var historyBefore = ReadScrollbarCore().HistoryCount;
- fixed (byte* bytes = data)
- {
- _callbackException = null;
- GhosttyNative.TerminalWrite(_terminal.DangerousGetHandle(), bytes, (nuint)data.Length);
- }
-
- if (_callbackException is { } callbackException)
- {
- throw new InvalidOperationException(
- "A libghostty-vt effect callback failed.",
- callbackException);
- }
-
- // Projecting the grid costs several native calls per cell, so it waits
- // for a reader (normally one snapshot per frame). Title and directory
- // changes still raise their events from the feed.
- _projectionStale = true;
- SynchronizeMetadata();
- var historyAfter = ReadScrollbarCore().HistoryCount;
- var initialEvictionPossible = historyBefore == 0 &&
- EstimateRows(data, Columns) >= historyAfter + Rows;
- var trackedHistoryEvicted = trackedHistory &&
- !GhosttyNative.TrackedGridRefHasValue(
- _historyAnchor!.DangerousGetHandle());
- if (trackedHistoryEvicted ||
- initialEvictionPossible ||
- !trackedHistory)
- {
- _buffer.AdvanceCoordinateVersion();
- }
- if (!trackedHistory || trackedHistoryEvicted)
- {
- ResetHistoryAnchor();
- }
- }
-
- RaiseInvalidated();
- }
-
- public void Feed(string text)
- {
- ArgumentNullException.ThrowIfNull(text);
- Feed(Encoding.UTF8.GetBytes(text));
- }
-
- public void Resize(int columns, int rows, double cellWidth = 1, double cellHeight = 1)
- {
- columns = Math.Clamp(columns, 1, ushort.MaxValue);
- rows = Math.Clamp(rows, 1, ushort.MaxValue);
- _cellWidthPixels = checked((uint)Math.Max(1, Math.Round(cellWidth)));
- _cellHeightPixels = checked((uint)Math.Max(1, Math.Round(cellHeight)));
- lock (_gate)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- ThrowIfFailed(
- GhosttyNative.TerminalResize(
- _terminal.DangerousGetHandle(),
- (ushort)columns,
- (ushort)rows,
- _cellWidthPixels,
- _cellHeightPixels),
- "ghostty_terminal_resize");
- _buffer.Resize(columns, rows);
- SynchronizeProjection();
- }
-
- RaiseInvalidated();
- }
-
- public void Reset()
- {
- lock (_gate)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- GhosttyNative.TerminalReset(_terminal.DangerousGetHandle());
- ResetHostKeyboardModes();
- SynchronizeProjection();
- ResetHistoryAnchor();
- }
-
- RaiseInvalidated();
- }
-
- public void SetScrollOffset(int offset)
- {
- lock (_gate)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- var scrollbar = ReadScrollbarCore();
- var normalized = Math.Clamp(offset, 0, scrollbar.HistoryCount);
- GhosttyNative.TerminalScrollViewport(
- _terminal.DangerousGetHandle(),
- new GhosttyScrollViewport
- {
- Tag = 3,
- Row = (nuint)(scrollbar.HistoryCount - normalized),
- });
- SynchronizeProjection();
- }
-
- RaiseInvalidated();
- }
-
- public void ConfigureOptionalFeatures(
- bool allowClipboardWrite,
- bool allowNotifications,
- bool allowKittyKeyboard = true)
- {
- _allowClipboardWrite = allowClipboardWrite;
- _allowNotifications = allowNotifications;
- _allowKittyKeyboard = allowKittyKeyboard;
- if (!allowKittyKeyboard)
- {
- _kittyKeyboardFlags = KittyKeyboardFlags.None;
- _kittyKeyboardStack.Clear();
- }
- }
-
- public TerminalSnapshot CreateSnapshot(bool includeHistory = false)
- {
- lock (_gate)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- EnsureProjection();
- var buffer = includeHistory ? CreateHistorySnapshot() : _buffer.CreateSnapshot();
- return new TerminalSnapshot(
- buffer,
- Title,
- WorkingDirectory,
- AlternateBufferActive,
- CursorVisible,
- ApplicationCursorKeys,
- BracketedPaste,
- MouseTracking,
- SgrMouse,
- AutoWrap,
- InsertMode,
- ReverseVideo);
- }
- }
-
- public string WrapPaste(string text)
- {
- ArgumentNullException.ThrowIfNull(text);
- return BracketedPaste
- ? "\u001b[200~" + text.Replace("\u001b", string.Empty, StringComparison.Ordinal) + "\u001b[201~"
- : text;
- }
-
- private void ResetHostKeyboardModes()
- {
- _ansiMode = true;
- _modifyOtherKeys = 0;
- _kittyKeyboardFlags = KittyKeyboardFlags.None;
- _kittyKeyboardStack.Clear();
- _keyboardModeScan = KeyboardModeScanState.Ground;
- ResetKeyboardCsi();
- }
-
- private void ResetKeyboardCsi()
- {
- _keyboardCsiPrivate = 0;
- _keyboardCsiValue = 0;
- _keyboardCsiHasValue = false;
- _keyboardCsiCount = 0;
- }
-
- private void TrackHostKeyboardModes(ReadOnlySpan data)
- {
- foreach (var value in data)
- {
- switch (_keyboardModeScan)
- {
- case KeyboardModeScanState.Ground:
- if (value == 0x1B)
- {
- _keyboardModeScan = KeyboardModeScanState.Escape;
- }
- break;
- case KeyboardModeScanState.Escape:
- if (value == (byte)'[')
- {
- _keyboardModeScan = KeyboardModeScanState.Csi;
- ResetKeyboardCsi();
- }
- else
- {
- _keyboardModeScan = KeyboardModeScanState.Ground;
- }
- break;
- case KeyboardModeScanState.Csi:
- if (value is 0x18 or 0x1A)
- {
- _keyboardModeScan = KeyboardModeScanState.Ground;
- }
- else if (_keyboardCsiPrivate == 0 &&
- !_keyboardCsiHasValue &&
- _keyboardCsiCount == 0 &&
- value is (byte)'?' or (byte)'>' or (byte)'<' or (byte)'=')
- {
- _keyboardCsiPrivate = value;
- }
- else if (value is >= (byte)'0' and <= (byte)'9')
- {
- _keyboardCsiHasValue = true;
- _keyboardCsiValue = _keyboardCsiValue > 100_000
- ? _keyboardCsiValue
- : (_keyboardCsiValue * 10) + (value - (byte)'0');
- }
- else if (value == (byte)';')
- {
- PushKeyboardCsiParam();
- }
- else if (value is >= 0x40 and <= 0x7E)
- {
- PushKeyboardCsiParam();
- ApplyKeyboardCsi(value);
- _keyboardModeScan = KeyboardModeScanState.Ground;
- }
- break;
- }
- }
- }
-
- private void PushKeyboardCsiParam()
- {
- if (_keyboardCsiCount >= _keyboardCsiParams.Length)
- {
- _keyboardCsiHasValue = false;
- _keyboardCsiValue = 0;
- return;
- }
-
- _keyboardCsiParams[_keyboardCsiCount++] = _keyboardCsiHasValue ? _keyboardCsiValue : 0;
- _keyboardCsiHasValue = false;
- _keyboardCsiValue = 0;
- }
-
- private void ApplyKeyboardCsi(byte final)
- {
- if (_keyboardCsiPrivate == (byte)'?' && final is (byte)'h' or (byte)'l')
- {
- var enable = final == (byte)'h';
- for (var index = 0; index < _keyboardCsiCount; index++)
- {
- switch (_keyboardCsiParams[index])
- {
- case 2:
- _ansiMode = enable;
- break;
- }
- }
- }
- else if (_keyboardCsiPrivate == (byte)'>' && final == (byte)'m' &&
- _keyboardCsiCount > 0 && _keyboardCsiParams[0] == 4)
- {
- _modifyOtherKeys = _keyboardCsiCount > 1
- ? Math.Clamp(_keyboardCsiParams[1], 0, 2)
- : 0;
- }
- else if (_allowKittyKeyboard && final == (byte)'u')
- {
- ApplyKittyKeyboardCsi();
- }
- }
-
- private void ApplyKittyKeyboardCsi()
- {
- const KittyKeyboardFlags supported =
- KittyKeyboardFlags.DisambiguateEscapeCodes |
- KittyKeyboardFlags.ReportEventTypes |
- KittyKeyboardFlags.ReportAllKeysAsEscapeCodes |
- KittyKeyboardFlags.ReportAssociatedText;
- switch (_keyboardCsiPrivate)
- {
- case (byte)'>':
- if (_kittyKeyboardStack.Count < 16)
- {
- _kittyKeyboardStack.Push(_kittyKeyboardFlags);
- }
-
- _kittyKeyboardFlags = (KittyKeyboardFlags)ParamKeyboard(0) & supported;
- break;
- case (byte)'<':
- var count = Math.Max(1, ParamKeyboard(0, 1));
- while (count-- > 0 && _kittyKeyboardStack.TryPop(out var restored))
- {
- _kittyKeyboardFlags = restored;
- }
-
- break;
- case (byte)'=':
- var flags = (KittyKeyboardFlags)ParamKeyboard(0) & supported;
- _kittyKeyboardFlags = ParamKeyboard(1, 1) switch
- {
- 2 => _kittyKeyboardFlags | flags,
- 3 => _kittyKeyboardFlags & ~flags,
- _ => flags,
- };
- break;
- }
- }
-
- private int ParamKeyboard(int index, int fallback = 0) =>
- index < _keyboardCsiCount ? _keyboardCsiParams[index] : fallback;
-
- private void ProbeUnsupportedImages(ReadOnlySpan data)
- {
- foreach (var value in data)
- {
- if (_imageProbeState != ImageProbeState.Ground && value is 0x18 or 0x1A)
- {
- _imageProbe.Clear();
- _dcsImageCandidate = false;
- _imageProbeState = ImageProbeState.Ground;
- continue;
- }
-
- switch (_imageProbeState)
- {
- case ImageProbeState.Ground:
- if (value == 0x1B)
- {
- _imageProbeState = ImageProbeState.Escape;
- }
- else if (value == 0x90)
- {
- _dcsImageCandidate = true;
- _imageProbeState = ImageProbeState.DcsHeader;
- }
- else if (value == 0x9D)
- {
- StartOscProbe();
- }
- else if (value == 0x9F)
- {
- _imageProbeState = ImageProbeState.Apc;
- }
- break;
- case ImageProbeState.Escape:
- if (value == (byte)'P')
- {
- _dcsImageCandidate = true;
- _imageProbeState = ImageProbeState.DcsHeader;
- }
- else if (value == (byte)']')
- {
- StartOscProbe();
- }
- else if (value == (byte)'_')
- {
- _imageProbeState = ImageProbeState.Apc;
- }
- else
- {
- _imageProbeState = ImageProbeState.Ground;
- }
- break;
- case ImageProbeState.Apc:
- if (value == (byte)'G')
- {
- // Chunked kitty transmissions repeat APC G per chunk;
- // report once per engine lifetime instead of per chunk.
- if (!_kittyImageReported)
- {
- _kittyImageReported = true;
- ReportUnsupportedImage(
- "image.kitty.unsupported",
- "The pinned libghostty-vt C ABI does not expose kitty graphics resources.");
- }
- }
-
- _imageProbeState = ImageProbeState.ApcIgnored;
- break;
- case ImageProbeState.ApcIgnored:
- if (value == 0x9C)
- {
- _imageProbeState = ImageProbeState.Ground;
- }
- else if (value == 0x1B)
- {
- _imageProbeState = ImageProbeState.ApcIgnoredEscape;
- }
- break;
- case ImageProbeState.ApcIgnoredEscape:
- _imageProbeState = value == (byte)'\\'
- ? ImageProbeState.Ground
- : ImageProbeState.ApcIgnored;
- break;
- case ImageProbeState.DcsHeader:
- if (value is >= 0x40 and <= 0x7E)
- {
- if (value == (byte)'q' && _dcsImageCandidate)
- {
- ReportUnsupportedImage(
- "image.sixel.unsupported",
- "The pinned libghostty-vt C ABI does not expose Sixel image resources.");
- }
- _imageProbeState = ImageProbeState.DcsBody;
- }
- else if (value is 0x18 or 0x1A)
- {
- _imageProbeState = ImageProbeState.Ground;
- }
- else if (value is >= 0x20 and <= 0x2F)
- {
- _dcsImageCandidate = false;
- }
- else if (value is >= 0x3A and <= 0x3F)
- {
- _dcsImageCandidate = false;
- }
- break;
- case ImageProbeState.DcsBody:
- if (value == 0x1B)
- {
- _imageProbeState = ImageProbeState.DcsEscape;
- }
- else if (value == 0x9C)
- {
- _imageProbeState = ImageProbeState.Ground;
- }
- break;
- case ImageProbeState.DcsEscape:
- _imageProbeState = value == (byte)'\\'
- ? ImageProbeState.Ground
- : ImageProbeState.DcsBody;
- break;
- case ImageProbeState.Osc:
- if (value is 0x07 or 0x9C)
- {
- _imageProbeState = ImageProbeState.Ground;
- }
- else if (value == 0x1B)
- {
- _imageProbeState = ImageProbeState.OscEscape;
- }
- else if (_imageProbe.Count < 16)
- {
- _imageProbe.Add(value);
- DetectUnsupportedOscImage();
- }
- break;
- case ImageProbeState.OscEscape:
- _imageProbeState = value == (byte)'\\'
- ? ImageProbeState.Ground
- : ImageProbeState.Osc;
- break;
- case ImageProbeState.OscIgnored:
- if (value is 0x07 or 0x9C)
- {
- _imageProbeState = ImageProbeState.Ground;
- }
- else if (value == 0x1B)
- {
- _imageProbeState = ImageProbeState.OscIgnoredEscape;
- }
- break;
- case ImageProbeState.OscIgnoredEscape:
- _imageProbeState = value == (byte)'\\'
- ? ImageProbeState.Ground
- : ImageProbeState.OscIgnored;
- break;
- }
- }
- }
-
- private void StartOscProbe()
- {
- _imageProbe.Clear();
- _imageProbeState = ImageProbeState.Osc;
- }
-
- private void DetectUnsupportedOscImage()
- {
- if (_imageProbe.Count == 5 && MatchesProbe("1337;"u8))
- {
- ReportUnsupportedImage(
- "image.osc1337.unsupported",
- "The pinned libghostty-vt C ABI does not expose OSC 1337 image resources.");
- _imageProbeState = ImageProbeState.OscIgnored;
- }
- else if (_imageProbe.Count == 7 && MatchesProbe("9;4;st="u8))
- {
- ReportUnsupportedImage(
- "image.conemu.unsupported",
- "The pinned libghostty-vt C ABI does not expose ConEmu image resources.");
- _imageProbeState = ImageProbeState.OscIgnored;
- }
- }
-
- private bool MatchesProbe(ReadOnlySpan expected)
- {
- if (_imageProbe.Count != expected.Length)
- {
- return false;
- }
-
- for (var index = 0; index < expected.Length; index++)
- {
- if (_imageProbe[index] != expected[index])
- {
- return false;
- }
- }
-
- return true;
- }
-
- private void ReportUnsupportedImage(string code, string message) =>
- Diagnostic?.Invoke(this, new TerminalEngineDiagnostic(code, message));
-
- public void Dispose()
- {
- lock (_gate)
- {
- if (_disposed)
- {
- return;
- }
-
- _disposed = true;
- _rowCells.Dispose();
- _rowIterator.Dispose();
- _renderState.Dispose();
- _historyAnchor?.Dispose();
- _terminal.Dispose();
- if (_selfHandle.IsAllocated)
- {
- _selfHandle.Free();
- }
- }
-
- GC.SuppressFinalize(this);
- }
-
- private void RegisterEffects()
- {
- var userData = (void*)GCHandle.ToIntPtr(_selfHandle);
- var terminal = _terminal.DangerousGetHandle();
- ThrowIfFailed(GhosttyNative.TerminalSet(terminal, TerminalOptionUserData, userData), "set userdata");
-
- delegate* unmanaged[Cdecl] writePty = &OnWritePty;
- ThrowIfFailed(GhosttyNative.TerminalSet(terminal, TerminalOptionWritePty, writePty), "set write PTY");
-
- delegate* unmanaged[Cdecl] bell = &OnBell;
- ThrowIfFailed(GhosttyNative.TerminalSet(terminal, TerminalOptionBell, bell), "set bell");
-
- delegate* unmanaged[Cdecl] stateChanged = &OnStateChanged;
- ThrowIfFailed(
- GhosttyNative.TerminalSet(terminal, TerminalOptionTitleChanged, stateChanged),
- "set title callback");
- ThrowIfFailed(
- GhosttyNative.TerminalSet(terminal, TerminalOptionPwdChanged, stateChanged),
- "set working-directory callback");
-
- delegate* unmanaged[Cdecl] size = &OnSize;
- ThrowIfFailed(
- GhosttyNative.TerminalSet(terminal, TerminalOptionSize, size),
- "set size callback");
-
- delegate* unmanaged[Cdecl] clipboardWrite = &OnClipboardWrite;
- ThrowIfFailed(
- GhosttyNative.TerminalSet(terminal, TerminalOptionClipboardWrite, clipboardWrite),
- "set clipboard callback");
-
- delegate* unmanaged[Cdecl] notification = &OnNotification;
- ThrowIfFailed(
- GhosttyNative.TerminalSet(terminal, TerminalOptionDesktopNotification, notification),
- "set notification callback");
- }
-
- private void ApplyScheme()
- {
- var foreground = ToRgb(_scheme.Foreground);
- var background = ToRgb(_scheme.Background);
- var cursor = ToRgb(_scheme.Cursor);
- ThrowIfFailed(
- GhosttyNative.TerminalSet(_terminal.DangerousGetHandle(), 11, &foreground),
- "set foreground");
- ThrowIfFailed(
- GhosttyNative.TerminalSet(_terminal.DangerousGetHandle(), 12, &background),
- "set background");
- ThrowIfFailed(
- GhosttyNative.TerminalSet(_terminal.DangerousGetHandle(), 13, &cursor),
- "set cursor");
-
- var palette = stackalloc GhosttyColorRgb[256];
- for (var index = 0; index < 256; index++)
- {
- palette[index] = ToRgb(_scheme.Resolve(index));
- }
-
- ThrowIfFailed(
- GhosttyNative.TerminalSet(_terminal.DangerousGetHandle(), 14, palette),
- "set palette");
- }
-
- private void EnsureProjection()
- {
- if (_projectionStale && !_disposed)
- {
- SynchronizeProjection(reprojectAll: false);
- }
- }
-
- ///
- /// Copies Ghostty's render state into . Rows the render
- /// state reports clean keep their previous projection: it only rebuilds a row
- /// (marking it dirty) when its contents change, and a viewport move, resize,
- /// screen switch or palette change rebuilds them all, so a clean row reads
- /// back exactly what was projected last time. Paths other than feeding pass
- /// to keep their previous, unconditional cost.
- ///
- private void SynchronizeProjection(bool reprojectAll = true)
- {
- ThrowIfFailed(
- GhosttyNative.RenderStateUpdate(
- _renderState.DangerousGetHandle(),
- _terminal.DangerousGetHandle()),
- "ghostty_render_state_update");
-
- var columns = GetRenderUInt16(RenderDataCols);
- var rows = GetRenderUInt16(RenderDataRows);
- if (_buffer.Columns != columns || _buffer.Rows != rows)
- {
- _buffer.Resize(columns, rows);
- }
-
- var frameDirty = 0;
- ThrowIfFailed(
- GhosttyNative.RenderStateGet(_renderState.DangerousGetHandle(), RenderDataDirty, &frameDirty),
- "get render dirty state");
- if (frameDirty == RenderStateDirtyFull ||
- _projectedRows.Length != rows ||
- (_projectedRows.Length > 0 && _projectedRows[0] is { } first && first.Cells.Length != columns))
- {
- reprojectAll = true;
- }
-
- if (_projectedRows.Length != rows)
- {
- _projectedRows = new (Cell[] Cells, int? PromptStart)?[rows];
- }
-
- var cursor = new GhosttyRenderCursor { Size = (nuint)sizeof(GhosttyRenderCursor) };
- ThrowIfFailed(
- GhosttyNative.RenderStateGet(_renderState.DangerousGetHandle(), RenderDataCursor, &cursor),
- "get render cursor");
- _cursor = cursor;
-
- var iteratorValue = _rowIterator.DangerousGetHandle();
- ThrowIfFailed(
- GhosttyNative.RenderStateGet(
- _renderState.DangerousGetHandle(),
- RenderDataRowIterator,
- &iteratorValue),
- "get row iterator");
-
- var projectedRows = new List(rows);
- var rowIndex = 0;
- while (GhosttyNative.RowIteratorNext(_rowIterator.DangerousGetHandle()))
- {
- ulong rawRow = 0;
- ThrowIfFailed(
- GhosttyNative.RowGet(
- _rowIterator.DangerousGetHandle(),
- RenderRowDataRaw,
- &rawRow),
- "get raw row");
- byte rowDirty = 1;
- if (!reprojectAll)
- {
- ThrowIfFailed(
- GhosttyNative.RowGet(
- _rowIterator.DangerousGetHandle(),
- RenderRowDataDirty,
- &rowDirty),
- "get row dirty");
- }
-
- byte wrapped = 0;
- ThrowIfFailed(GhosttyNative.RawRowGet(rawRow, 1, &wrapped), "get row wrap");
- var semanticPrompt = 0;
- ThrowIfFailed(
- GhosttyNative.RawRowGet(rawRow, 6, &semanticPrompt),
- "get row semantic prompt");
- var cached = rowIndex < _projectedRows.Length ? _projectedRows[rowIndex] : null;
- if (rowDirty != 0 || cached is null)
- {
- var cellsValue = _rowCells.DangerousGetHandle();
- ThrowIfFailed(
- GhosttyNative.RowGet(
- _rowIterator.DangerousGetHandle(),
- RenderRowDataCells,
- &cellsValue),
- "get row cells");
- cached = ProjectRow(columns, rowIndex);
- if (rowIndex < _projectedRows.Length)
- {
- _projectedRows[rowIndex] = cached;
- }
- }
-
- var projected = cached.Value;
- projectedRows.Add(new TextBufferProjectionRow(
- projected.Cells,
- wrapped != 0,
- semanticPrompt == 1 && projected.PromptStart is { } promptStart
- ? [new ShellMark(promptStart)]
- : []));
- rowIndex++;
- }
-
- while (projectedRows.Count < rows)
- {
- projectedRows.Add(new TextBufferProjectionRow(
- Enumerable.Repeat(Cell.Blank, columns).ToArray(),
- false,
- []));
- }
-
- var cursorX = _cursor.ViewportHasValue != 0
- ? _cursor.ViewportX
- : QueryUInt16Core(3);
- var cursorY = _cursor.ViewportHasValue != 0
- ? _cursor.ViewportY
- : QueryUInt16Core(4);
- _buffer.ReplaceViewport(
- projectedRows,
- cursorX,
- cursorY);
- // Every row was consumed, so the next update reports only new changes.
- ThrowIfFailed(
- GhosttyNative.RenderStateClean(_renderState.DangerousGetHandle()),
- "ghostty_render_state_clean");
- _projectionStale = false;
- SynchronizeMetadata();
- }
-
- private (Cell[] Cells, int? PromptStart) ProjectRow(int columns, int rowIndex)
- {
- var row = Enumerable.Repeat(Cell.Blank, columns).ToArray();
- var column = 0;
- var previousWidth = 0;
- int? promptStart = null;
- while (column < columns && GhosttyNative.RowCellsNext(_rowCells.DangerousGetHandle()))
- {
- ulong rawCell = 0;
- ThrowIfFailed(
- GhosttyNative.RowCellsGet(
- _rowCells.DangerousGetHandle(),
- RenderCellDataRaw,
- &rawCell),
- "get raw cell");
- var wide = GetRawCellInt(rawCell, 3);
- var style = new GhosttyStyle { Size = (nuint)sizeof(GhosttyStyle) };
- ThrowIfFailed(
- GhosttyNative.RowCellsGet(
- _rowCells.DangerousGetHandle(),
- RenderCellDataStyle,
- &style),
- "get cell style");
- var hasHyperlink = GetRawCellBool(rawCell, 7);
- var semantic = GetRawCellInt(rawCell, 9);
- var projected = Cell.Blank with
- {
- Attributes = new CellAttributes
- {
- Foreground = GetCellColor(RenderCellDataForeground),
- Background = GetCellColor(RenderCellDataBackground),
- Flags = ToCellFlags(style),
- },
- IsProtected = GetRawCellBool(rawCell, 8),
- HyperlinkUri = hasHyperlink
- ? GetHyperlinkUri(column, rowIndex, ref _hyperlinkBuffer)
- : null,
- ShellIntegration = ShellIntegrationKind.None,
- };
- uint graphemeLength = 0;
- ThrowIfFailed(
- GhosttyNative.RowCellsGet(
- _rowCells.DangerousGetHandle(),
- RenderCellDataGraphemeLength,
- &graphemeLength),
- "get grapheme length");
- if (graphemeLength == 0)
- {
- if (wide == 2 || previousWidth == 2)
- {
- projected.IsWideContinuation = true;
- projected.StoredWidth = 0;
- }
-
- row[column] = projected;
- previousWidth = 0;
- column++;
- continue;
- }
-
- if (_codepointBuffer.Length < graphemeLength)
- {
- _codepointBuffer = new uint[graphemeLength];
- }
-
- fixed (uint* codepoints = _codepointBuffer)
- {
- ThrowIfFailed(
- GhosttyNative.RowCellsGet(
- _rowCells.DangerousGetHandle(),
- RenderCellDataGraphemeBuffer,
- codepoints),
- "get grapheme");
- }
-
- var rune = Rune.TryCreate(_codepointBuffer[0], out var value) ? value : Rune.ReplacementChar;
- string? combining = null;
- if (graphemeLength > 1)
- {
- var builder = new StringBuilder();
- for (var index = 1; index < graphemeLength; index++)
- {
- if (Rune.TryCreate(_codepointBuffer[index], out var extra))
- {
- builder.Append(extra);
- }
- }
-
- combining = builder.Length == 0 ? null : builder.ToString();
- }
-
- var width = wide == 1 ? 2 : 1;
- projected.Rune = rune;
- projected.CombiningCharacters = combining;
- projected.StoredWidth = (byte)width;
- projected.ShellIntegration = semantic switch
- {
- 1 => ShellIntegrationKind.Command,
- 2 => ShellIntegrationKind.Prompt,
- _ => ShellIntegrationKind.Output,
- };
- if (semantic == 2)
- {
- promptStart ??= column;
- }
-
- row[column] = projected;
- previousWidth = width;
- column++;
- }
-
- return (row, promptStart);
- }
-
- private static bool GetRawCellBool(ulong cell, int data)
- {
- byte value = 0;
- ThrowIfFailed(GhosttyNative.CellGet(cell, data, &value), "get raw cell flag");
- return value != 0;
- }
-
- private static int GetRawCellInt(ulong cell, int data)
- {
- var value = 0;
- ThrowIfFailed(GhosttyNative.CellGet(cell, data, &value), "get raw cell value");
- return value;
- }
-
- private string? GetHyperlinkUri(int column, int row, ref byte[] buffer)
- {
- var gridRef = new GhosttyGridRef { Size = (nuint)sizeof(GhosttyGridRef) };
- var result = GhosttyNative.TerminalGridRef(
- _terminal.DangerousGetHandle(),
- new GhosttyPoint
- {
- Tag = 1,
- X = checked((ushort)column),
- Y = checked((uint)row),
- },
- &gridRef);
- if (result != GhosttyResult.Success)
- {
- return null;
- }
-
- nuint written = 0;
- fixed (byte* bytes = buffer)
- {
- result = GhosttyNative.GridRefHyperlinkUri(
- &gridRef,
- bytes,
- (nuint)buffer.Length,
- &written);
- }
-
- if (result == GhosttyResult.OutOfSpace)
- {
- buffer = new byte[checked((int)written)];
- fixed (byte* bytes = buffer)
- {
- result = GhosttyNative.GridRefHyperlinkUri(
- &gridRef,
- bytes,
- (nuint)buffer.Length,
- &written);
- }
- }
-
- if (result != GhosttyResult.Success || written == 0)
- {
- return null;
- }
-
- return Encoding.UTF8.GetString(buffer, 0, checked((int)written));
- }
-
- private TermColor GetCellColor(int data)
- {
- GhosttyColorRgb color;
- var result = GhosttyNative.RowCellsGet(_rowCells.DangerousGetHandle(), data, &color);
- return result == GhosttyResult.Success
- ? TermColor.FromRgb(color.R, color.G, color.B)
- : TermColor.Default;
- }
-
- private void SynchronizeMetadata()
- {
- var rawTitle = QueryString(TerminalDataTitle);
- var title = string.IsNullOrEmpty(rawTitle) ? "Devolutions Terminal" : rawTitle;
- if (!string.Equals(_title, title, StringComparison.Ordinal))
- {
- _title = title;
- TitleChanged?.Invoke(this, _title);
- }
-
- var rawWorkingDirectory = QueryString(TerminalDataPwd);
- var workingDirectory = string.IsNullOrEmpty(rawWorkingDirectory)
- ? null
- : rawWorkingDirectory;
- if (!string.Equals(_workingDirectory, workingDirectory, StringComparison.Ordinal))
- {
- _workingDirectory = workingDirectory;
- WorkingDirectoryChanged?.Invoke(this, _workingDirectory);
- ShellIntegrationChanged?.Invoke(this, EventArgs.Empty);
- }
- }
-
- private ushort GetRenderUInt16(int data)
- {
- ushort value = 0;
- ThrowIfFailed(
- GhosttyNative.RenderStateGet(_renderState.DangerousGetHandle(), data, &value),
- "get render dimension");
- return value;
- }
-
- private int QueryInt(int data)
- {
- lock (_gate)
- {
- var value = 0;
- ThrowIfFailed(
- GhosttyNative.TerminalGet(_terminal.DangerousGetHandle(), data, &value),
- "get terminal value");
- return value;
- }
- }
-
- private ushort QueryUInt16Core(int data)
- {
- ushort value = 0;
- ThrowIfFailed(
- GhosttyNative.TerminalGet(_terminal.DangerousGetHandle(), data, &value),
- "get terminal coordinate");
- return value;
- }
-
- private uint QueryUInt32(int data)
- {
- lock (_gate)
- {
- uint value = 0;
- ThrowIfFailed(
- GhosttyNative.TerminalGet(_terminal.DangerousGetHandle(), data, &value),
- "get terminal pixel dimension");
- return value;
- }
- }
-
- private (int HistoryCount, int ScrollOffset) ReadScrollbar()
- {
- lock (_gate)
- {
- return ReadScrollbarCore();
- }
- }
-
- private (int HistoryCount, int ScrollOffset) ReadScrollbarCore()
- {
- var value = new GhosttyScrollbar();
- ThrowIfFailed(
- GhosttyNative.TerminalGet(_terminal.DangerousGetHandle(), 9, &value),
- "get scrollbar");
- var historyRows = value.Total > value.Length ? value.Total - value.Length : 0;
- var history = checked((int)Math.Min(historyRows, int.MaxValue));
- var top = checked((int)Math.Min(value.Offset, int.MaxValue));
- return (history, Math.Clamp(history - top, 0, history));
- }
-
- private void ResetHistoryAnchor()
- {
- _historyAnchor?.Dispose();
- _historyAnchor = null;
- var pointTag = ReadScrollbarCore().HistoryCount == 0 ? 0 : 3;
- var result = GhosttyNative.TerminalGridRefTrack(
- _terminal.DangerousGetHandle(),
- new GhosttyPoint { Tag = pointTag },
- out var tracked);
- if (result == GhosttyResult.Success)
- {
- _historyAnchor = new SafeGhosttyTrackedGridRefHandle(tracked);
- return;
- }
-
- ThrowIfFailed(result, "track history origin");
- }
-
- private TextBufferSnapshot CreateHistorySnapshot()
- {
- var scrollbar = ReadScrollbarCore();
- if (scrollbar.HistoryCount == 0)
- {
- return _buffer.CreateSnapshot();
- }
-
- var originalOffset = scrollbar.ScrollOffset;
- var lines = new List(scrollbar.HistoryCount + Rows);
- var nextAbsoluteRow = 0;
- for (var top = 0; top <= scrollbar.HistoryCount;)
- {
- ScrollToAbsoluteRow(top);
- var viewport = _buffer.CreateSnapshot();
- var firstNewRow = Math.Max(0, nextAbsoluteRow - top);
- for (var row = firstNewRow; row < viewport.Lines.Count; row++)
- {
- lines.Add(viewport.Lines[row]);
- }
-
- nextAbsoluteRow = top + viewport.Rows;
- if (top == scrollbar.HistoryCount)
- {
- break;
- }
-
- top = Math.Min(scrollbar.HistoryCount, top + viewport.Rows);
- }
-
- ScrollToAbsoluteRow(scrollbar.HistoryCount - originalOffset);
- return new TextBufferSnapshot(
- Columns,
- Rows,
- CursorX,
- CursorY,
- scrollbar.HistoryCount,
- originalOffset,
- lines);
- }
-
- private void ScrollToAbsoluteRow(int row)
- {
- GhosttyNative.TerminalScrollViewport(
- _terminal.DangerousGetHandle(),
- new GhosttyScrollViewport
- {
- Tag = 3,
- Row = (nuint)Math.Max(0, row),
- });
- SynchronizeProjection();
- }
-
- private bool QueryMode(ushort value, bool ansi = false)
- {
- lock (_gate)
- {
- var config = new GhosttyModeConfig
- {
- Mode = (ushort)(value | (ansi ? 0x8000 : 0)),
- };
- var result = GhosttyNative.TerminalGet(
- _terminal.DangerousGetHandle(),
- TerminalDataMode,
- &config);
- return result == GhosttyResult.Success && config.Value != 0;
- }
- }
-
- private string QueryString(int data)
- {
- var value = new GhosttyString();
- var result = GhosttyNative.TerminalGet(_terminal.DangerousGetHandle(), data, &value);
- if (result == GhosttyResult.NoValue || value.Pointer == 0 || value.Length == 0)
- {
- return string.Empty;
- }
-
- ThrowIfFailed(result, "get terminal string");
- return Encoding.UTF8.GetString(
- new ReadOnlySpan((void*)value.Pointer, checked((int)value.Length)));
- }
-
- private static CellFlags ToCellFlags(GhosttyStyle style)
- {
- var flags = CellFlags.None;
- if (style.Bold != 0) flags |= CellFlags.Bold;
- if (style.Faint != 0) flags |= CellFlags.Faint;
- if (style.Italic != 0) flags |= CellFlags.Italic;
- if (style.Underline != 0) flags |= CellFlags.Underline;
- if (style.Blink != 0) flags |= CellFlags.Blink;
- if (style.Inverse != 0) flags |= CellFlags.Inverse;
- if (style.Invisible != 0) flags |= CellFlags.Invisible;
- if (style.Strikethrough != 0) flags |= CellFlags.Strikethrough;
- return flags;
- }
-
- private static GhosttyColorRgb ToRgb(uint color) => new()
- {
- R = (byte)(color >> 16),
- G = (byte)(color >> 8),
- B = (byte)color,
- };
-
- private void RaiseInvalidated()
- {
- try
- {
- Invalidated?.Invoke(this, EventArgs.Empty);
- }
- catch (Exception)
- {
- // Subscribers (scrollbar, UIA) must not fail the PTY output pump.
- }
- }
-
- private static void ThrowIfFailed(GhosttyResult result, string operation)
- {
- if (result != GhosttyResult.Success)
- {
- throw new GhosttyException(operation, result);
- }
- }
-
- [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static void OnWritePty(nint terminal, nint userData, byte* data, nuint length)
- {
- if (TryGetInstance(userData) is { } instance)
- {
- try
- {
- instance.ResponseReady?.Invoke(
- instance,
- new ReadOnlySpan(data, checked((int)length)).ToArray());
- }
- catch (Exception exception)
- {
- instance._callbackException ??= exception;
- }
- }
- }
-
- [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static void OnBell(nint terminal, nint userData)
- {
- if (TryGetInstance(userData) is { } instance)
- {
- try
- {
- instance.Bell?.Invoke(instance, EventArgs.Empty);
- }
- catch (Exception exception)
- {
- instance._callbackException ??= exception;
- }
- }
- }
-
- [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static void OnStateChanged(nint terminal, nint userData)
- {
- }
-
- [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static byte OnSize(nint terminal, nint userData, GhosttySizeReport* size)
- {
- var instance = TryGetInstance(userData);
- if (instance is null || size is null)
- {
- return 0;
- }
-
- size->Rows = checked((ushort)instance.Rows);
- size->Columns = checked((ushort)instance.Columns);
- size->CellWidth = instance._cellWidthPixels;
- size->CellHeight = instance._cellHeightPixels;
- return 1;
- }
-
- [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static void OnClipboardWrite(
- nint terminal,
- nint userData,
- GhosttyClipboardWrite* write)
- {
- var instance = TryGetInstance(userData);
- var result = 1;
- if (instance?._allowClipboardWrite == true && write is not null)
- {
- string? text = write->ContentsLength == 0 ? string.Empty : null;
- for (nuint index = 0; index < write->ContentsLength; index++)
- {
- var content = write->Contents[index];
- var mime = ReadString(content.Mime);
- if (mime.Equals("text/plain", StringComparison.OrdinalIgnoreCase) ||
- mime.StartsWith("text/plain;", StringComparison.OrdinalIgnoreCase))
- {
- text = ReadString(content.Data);
- break;
- }
- }
-
- if (text is not null)
- {
- try
- {
- instance.ClipboardWriteRequested?.Invoke(instance, text);
- // Avalonia's clipboard API is asynchronous, while libghostty
- // requires this callback to reply synchronously. OSC 52 ignores
- // this status; acknowledged protocols are explicitly unsupported.
- result = 2;
- }
- catch (Exception exception)
- {
- instance._callbackException ??= exception;
- result = 5;
- }
- }
- else
- {
- result = 2;
- }
- }
-
- if (write is not null && write->Reply != 0)
- {
- var reply = new GhosttyClipboardWriteReply
- {
- Size = (nuint)sizeof(GhosttyClipboardWriteReply),
- Result = result,
- };
- var callback =
- (delegate* unmanaged[Cdecl])write->Reply;
- callback(write, &reply);
- }
- }
-
- [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static void OnNotification(
- nint terminal,
- nint userData,
- GhosttyDesktopNotification* notification)
- {
- var instance = TryGetInstance(userData);
- if (instance?._allowNotifications != true || notification is null)
- {
- return;
- }
-
- try
- {
- instance.NotificationRequested?.Invoke(
- instance,
- new TerminalNotification(
- ReadString(notification->Title),
- ReadString(notification->Body)));
- }
- catch (Exception exception)
- {
- instance._callbackException ??= exception;
- }
- }
-
- private static string ReadString(GhosttyString value) =>
- value.Pointer == 0 || value.Length == 0
- ? string.Empty
- : Encoding.UTF8.GetString(
- new ReadOnlySpan((void*)value.Pointer, checked((int)value.Length)));
-
- private static GhosttyTerminalEngine? TryGetInstance(nint userData) =>
- userData == 0 ? null : GCHandle.FromIntPtr(userData).Target as GhosttyTerminalEngine;
-
- private static int EstimateRows(ReadOnlySpan data, int columns)
- {
- var rows = 0;
- var printable = 0;
- for (var index = 0; index < data.Length; index++)
- {
- var value = data[index];
- if (value is (byte)'\n' or 0x0B or 0x0C)
- {
- rows++;
- }
- else if (value == 0x1B &&
- index + 1 < data.Length &&
- data[index + 1] is (byte)'D' or (byte)'E' or (byte)'M')
- {
- rows++;
- index++;
- }
- else if (value >= 0x20 && value != 0x7F)
- {
- printable++;
- }
- }
-
- return rows + (printable / Math.Max(1, columns));
- }
-
-}
diff --git a/src/Devolutions.Terminal.Package/Scripts/Test-PackageBuild.ps1 b/src/Devolutions.Terminal.Package/Scripts/Test-PackageBuild.ps1
index 454cb7b..7122b1e 100644
--- a/src/Devolutions.Terminal.Package/Scripts/Test-PackageBuild.ps1
+++ b/src/Devolutions.Terminal.Package/Scripts/Test-PackageBuild.ps1
@@ -83,13 +83,12 @@ function New-PackageFixture {
[BitConverter]::GetBytes([uint32]0x60000020).CopyTo($image, 0x1AC)
$machine = if ($architecture -eq "arm64") { [uint16]0xAA64 } else { [uint16]0x8664 }
[BitConverter]::GetBytes($machine).CopyTo($image, 0x84)
- foreach ($name in @("Devolutions.Terminal.exe", "dt.exe", "ghostty-vt.dll")) {
+ foreach ($name in @("Devolutions.Terminal.exe", "dt.exe")) {
[IO.File]::WriteAllBytes((Join-Path $layout $name), $image)
}
foreach ($name in @("Devolutions.Terminal.ShellExt.dll", "dt-shell-integration.exe")) {
[IO.File]::WriteAllBytes((Join-Path $native $name), $image)
}
- [IO.File]::WriteAllText((Join-Path $layout "THIRD-PARTY-NOTICES-GHOSTTY.txt"), "Fixture license")
}
}
diff --git a/src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1 b/src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1
index a3e753a..37388b0 100644
--- a/src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1
+++ b/src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1
@@ -120,11 +120,7 @@ begin {
$manifestPath = Join-Path $extractPath "AppxManifest.xml"
Assert-Condition (Test-Path -LiteralPath $manifestPath) "AppxManifest.xml is missing from '$Path'."
Assert-Condition (Test-Path -LiteralPath (Join-Path $extractPath "Devolutions.Terminal.exe")) "Devolutions.Terminal.exe is missing from '$Path'."
- $ghosttyPath = Join-Path $extractPath "ghostty-vt.dll"
- Assert-Condition (Test-Path -LiteralPath $ghosttyPath) "ghostty-vt.dll is missing from '$Path'."
- Assert-Condition (
- Test-Path -LiteralPath (Join-Path $extractPath "THIRD-PARTY-NOTICES-GHOSTTY.txt")
- ) "The Ghostty license notice is missing from '$Path'."
+
Assert-Condition (
Test-Path -LiteralPath (Join-Path $extractPath "SHELL-INTEGRATION-NOTICE.txt")
) "The shell integration notice is missing from '$Path'."
@@ -147,10 +143,7 @@ begin {
Assert-Condition ($identity.Publisher -ceq $ExpectedPublisher) "Unexpected package publisher '$($identity.Publisher)'; expected '$ExpectedPublisher'."
Assert-Condition ($identity.ProcessorArchitecture -in @("x64", "arm64")) "Unexpected package architecture '$($identity.ProcessorArchitecture)'."
$expectedMachine = if ($identity.ProcessorArchitecture -eq "arm64") { 0xAA64 } else { 0x8664 }
- $ghosttyMachine = Get-PeMachine $ghosttyPath
- Assert-Condition (
- $ghosttyMachine -eq $expectedMachine
- ) "ghostty-vt.dll architecture 0x$($ghosttyMachine.ToString('X4')) does not match '$($identity.ProcessorArchitecture)' in '$Path'."
+
foreach ($helper in @("Devolutions.Terminal.ShellExt.dll", "dt-shell-integration.exe")) {
$helperPath = Join-Path $extractPath $helper
Assert-Condition (Test-Path -LiteralPath $helperPath -PathType Leaf) "'$helper' is missing from '$Path'."
diff --git a/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs b/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs
index de21f87..55fd067 100644
--- a/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs
+++ b/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs
@@ -212,8 +212,6 @@ private void Change(T oldValue, T newValue, Action update)
public sealed class ProfileItemViewModel(ProfileSettings profile, Action changed, bool supportsPowerShellIse = true,
Func? prepareTypeChange = null) : ObservableObject
{
- public static IReadOnlyList TerminalEngineChoices { get; } =
- ["Inherit", "Built-in", "Ghostty"];
private string _environmentJson = SerializeEnvironment(profile.Environment);
public string DisplayName => IsDefaults ? "Defaults" : $"{profile.Name} ({profile.Origin})";
@@ -295,24 +293,6 @@ public string ProfileType
public string Name { get => IsDefaults ? "Defaults" : profile.Name; set { if (!IsDefaults) Change(profile.Name, value, v => profile.Name = v); } }
public string? Guid => profile.Guid;
public string? Source => profile.Source;
- public string TerminalEngine
- {
- get => profile.TerminalEngine switch
- {
- TerminalEngineKind.BuiltIn => "Built-in",
- TerminalEngineKind.Ghostty => "Ghostty",
- _ => "Inherit",
- };
- set => Change(
- profile.TerminalEngine,
- value.ToLowerInvariant() switch
- {
- "built-in" or "builtin" => TerminalEngineKind.BuiltIn,
- "ghostty" => TerminalEngineKind.Ghostty,
- _ => null,
- },
- v => profile.TerminalEngine = v);
- }
public bool Hidden { get => profile.Hidden; set => Change(profile.Hidden, value, v => profile.Hidden = v); }
public string Commandline { get => profile.Commandline; set => Change(profile.Commandline, value, v => profile.Commandline = v); }
public string StartingDirectory { get => profile.StartingDirectory; set => Change(profile.StartingDirectory, value, v => profile.StartingDirectory = v); }
@@ -863,17 +843,6 @@ private static JsonObject SerializeKnownEntry(NewTabMenuEntry entry)
public sealed class RenderingSettingsViewModel(AppSettings settings, Action changed)
: SettingsPageViewModel("Rendering", "Configure the graphics API and rendering fallbacks.")
{
- public IReadOnlyList TerminalEngines { get; } = ["Built-in", "Ghostty"];
- public string TerminalEngine
- {
- get => settings.TerminalEngine == TerminalEngineKind.Ghostty ? "Ghostty" : "Built-in";
- set => Change(
- settings.TerminalEngine,
- value.Equals("Ghostty", StringComparison.OrdinalIgnoreCase)
- ? TerminalEngineKind.Ghostty
- : TerminalEngineKind.BuiltIn,
- v => settings.TerminalEngine = v);
- }
public string GraphicsApi { get => settings.GraphicsApi; set => Change(settings.GraphicsApi, value, v => settings.GraphicsApi = v); }
public IReadOnlyList GraphicsApiChoices { get; } = ["automatic", "direct3d11", "opengl", "software"];
public bool DisablePartialInvalidation { get => settings.DisablePartialInvalidation; set => Change(settings.DisablePartialInvalidation, value, v => settings.DisablePartialInvalidation = v); }
diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml b/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml
index 0a14bf2..ea78457 100644
--- a/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml
+++ b/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml
@@ -641,17 +641,11 @@
-
-
-
-
-
-
-
+
+
+
+
+
@@ -762,15 +756,9 @@
-
-
+
+
-
-
["rendering.graphicsAPI"] = settings.GraphicsApi,
["rendering.disablePartialInvalidation"] = settings.DisablePartialInvalidation,
["rendering.software"] = settings.SoftwareRendering,
- ["experimental.terminalEngine"] = TerminalEngineName(settings.TerminalEngine),
["compatibility.textMeasurement"] = settings.TextMeasurement,
["compatibility.ambiguousWidth"] = settings.AmbiguousWidth,
["defaultInputScope"] = settings.DefaultInputScope,
@@ -1552,14 +1549,6 @@ private static JsonObject SerializeProfile(ProfileSettings profile, bool include
}
result["ise"] = ise;
}
- if (profile.TerminalEngine is { } terminalEngine)
- {
- result["experimental.terminalEngine"] = TerminalEngineName(terminalEngine);
- }
- else
- {
- result.Remove("experimental.terminalEngine");
- }
result["colorScheme"] = ColorSchemeNode(profile.DarkColorScheme, profile.LightColorScheme);
result["font"] = new JsonObject
{
@@ -2105,22 +2094,6 @@ private static CopyFormat CopyFormats(JsonObject root)
_ => JsonValue.Create(false),
};
- private static TerminalEngineKind? TerminalEngineValue(JsonNode? node) =>
- node is JsonValue value && value.TryGetValue(out var name)
- ? name.ToLowerInvariant() switch
- {
- "builtin" or "built-in" or "windows-terminal" => TerminalEngineKind.BuiltIn,
- "ghostty" => TerminalEngineKind.Ghostty,
- _ => null,
- }
- : null;
-
- private static string TerminalEngineName(TerminalEngineKind engine) => engine switch
- {
- TerminalEngineKind.Ghostty => "ghostty",
- _ => "builtin",
- };
-
private static JsonArray StringArray(IEnumerable values) =>
new(values.Select(static value => (JsonNode?)JsonValue.Create(value)).ToArray());
diff --git a/src/Devolutions.Terminal/Devolutions.Terminal.csproj b/src/Devolutions.Terminal/Devolutions.Terminal.csproj
index 6124e9b..dab3d74 100644
--- a/src/Devolutions.Terminal/Devolutions.Terminal.csproj
+++ b/src/Devolutions.Terminal/Devolutions.Terminal.csproj
@@ -23,11 +23,6 @@
-
+ Condition="'%(ResolvedFileToPublish.AssetType)' == 'native' or '%(ResolvedFileToPublish.Filename)' == 'OpenConsole' or ('%(ResolvedFileToPublish.Filename)' == 'dt' and '%(ResolvedFileToPublish.Extension)' == '.exe') or '%(ResolvedFileToPublish.Filename)' == 'dt-pty-host'">
true
diff --git a/tests/Devolutions.Terminal.App.Tests/Devolutions.Terminal.App.Tests.csproj b/tests/Devolutions.Terminal.App.Tests/Devolutions.Terminal.App.Tests.csproj
index 06204f1..3b5db2c 100644
--- a/tests/Devolutions.Terminal.App.Tests/Devolutions.Terminal.App.Tests.csproj
+++ b/tests/Devolutions.Terminal.App.Tests/Devolutions.Terminal.App.Tests.csproj
@@ -1,14 +1,10 @@
-
- ghostty
-
- key == "VERSION" || key.EndsWith("_VERSION", StringComparison.Ordinal));
diff --git a/tests/Devolutions.Terminal.App.Tests/PlatformLauncherTests.cs b/tests/Devolutions.Terminal.App.Tests/PlatformLauncherTests.cs
index f81200a..13c090b 100644
--- a/tests/Devolutions.Terminal.App.Tests/PlatformLauncherTests.cs
+++ b/tests/Devolutions.Terminal.App.Tests/PlatformLauncherTests.cs
@@ -281,7 +281,6 @@ public void MacOsNotificationsUseOsascript()
Assert.Equal(["-e", "display notification \"Complete\" with title \"Build\""], command.ArgumentList);
Assert.Contains("osascript", launcher.GetCapabilityReport(), StringComparison.Ordinal);
Assert.Contains("dt-pty-host", launcher.GetCapabilityReport(), StringComparison.Ordinal);
- Assert.Contains("Ghostty engine:", launcher.GetCapabilityReport(), StringComparison.Ordinal);
Assert.DoesNotContain("not bundled yet", launcher.GetCapabilityReport(), StringComparison.Ordinal);
}
diff --git a/tests/Devolutions.Terminal.App.Tests/TerminalEngineFactoryTests.cs b/tests/Devolutions.Terminal.App.Tests/TerminalEngineFactoryTests.cs
index 86be3d6..62dbfca 100644
--- a/tests/Devolutions.Terminal.App.Tests/TerminalEngineFactoryTests.cs
+++ b/tests/Devolutions.Terminal.App.Tests/TerminalEngineFactoryTests.cs
@@ -1,5 +1,4 @@
using Devolutions.Terminal.Core;
-using Devolutions.Terminal.Ghostty;
using Devolutions.Terminal.Settings;
using Devolutions.Terminal.App.Connections;
using Xunit;
@@ -9,7 +8,7 @@ namespace Devolutions.Terminal.App.Tests;
public sealed class TerminalEngineFactoryTests
{
[Fact]
- public void BuiltInRemainsTheDefault()
+ public void CreatesTheBuiltInEngine()
{
using var engine = TerminalEngineFactory.Create(new AppSettings(), new ProfileSettings());
@@ -17,39 +16,13 @@ public void BuiltInRemainsTheDefault()
}
[Fact]
- public void GlobalGhosttySelectionCreatesGhosttyEngine()
+ public void LegacyGhosttySelectionLoadsTheBuiltInEngine()
{
- using var engine = TerminalEngineFactory.Create(
- new AppSettings { TerminalEngine = TerminalEngineKind.Ghostty },
- new ProfileSettings());
+ const string user = """{ "experimental.terminalEngine": "ghostty" }""";
+ var settings = SettingsLoader.Load(SettingsLoader.ReadEmbeddedDefaults(), user);
- Assert.IsType(engine);
- }
-
- [Fact]
- public void ProfileSelectionOverridesGlobalEngine()
- {
- using var engine = TerminalEngineFactory.Create(
- new AppSettings { TerminalEngine = TerminalEngineKind.Ghostty },
- new ProfileSettings { TerminalEngine = TerminalEngineKind.BuiltIn });
+ using var engine = TerminalEngineFactory.Create(settings, new ProfileSettings());
Assert.IsType(engine);
}
-
- [Fact]
- public void ProfileDefaultsOverrideGlobalEngineWhenProfileInherits()
- {
- using var engine = TerminalEngineFactory.Create(
- new AppSettings
- {
- TerminalEngine = TerminalEngineKind.BuiltIn,
- ProfileDefaults = new ProfileSettings
- {
- TerminalEngine = TerminalEngineKind.Ghostty,
- },
- },
- new ProfileSettings());
-
- Assert.IsType(engine);
- }
}
diff --git a/tests/Devolutions.Terminal.Ghostty.Tests/Devolutions.Terminal.Ghostty.Tests.csproj b/tests/Devolutions.Terminal.Ghostty.Tests/Devolutions.Terminal.Ghostty.Tests.csproj
deleted file mode 100644
index 39128a6..0000000
--- a/tests/Devolutions.Terminal.Ghostty.Tests/Devolutions.Terminal.Ghostty.Tests.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- ghostty
-
-
-
-
-
-
-
-
diff --git a/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyConPtyIntegrationTests.cs b/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyConPtyIntegrationTests.cs
deleted file mode 100644
index 8ab4e2b..0000000
--- a/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyConPtyIntegrationTests.cs
+++ /dev/null
@@ -1,338 +0,0 @@
-using System.Runtime.Versioning;
-using System.Text;
-using Devolutions.Terminal.Connection;
-using Devolutions.Terminal.Core;
-using Xunit;
-
-namespace Devolutions.Terminal.Ghostty.Tests;
-
-[SupportedOSPlatform("windows")]
-public sealed class GhosttyConPtyIntegrationTests
-{
- public static bool IsWindows => OperatingSystem.IsWindows();
-
- [Fact(Skip = "ConPTY is Windows-only.", SkipUnless = nameof(IsWindows))]
- public async Task CmdEchoProjectsIntoGhosttyGrid()
- {
- var marker = "GHOSTTY_CONPTY_OK";
- await AssertGhosttyProjectsConPtyAsync(
- EchoCommand(marker),
- writeCommands: [],
- marker,
- TestContext.Current.CancellationToken);
- }
-
- [Fact(Skip = "ConPTY is Windows-only.", SkipUnless = nameof(IsWindows))]
- public async Task WindowsPowerShellMarkerProjectsIntoGhosttyGrid()
- {
- var marker = "GHOSTTY_PS_OK";
- await AssertGhosttyProjectsConPtyAsync(
- WindowsPowerShellCommand("-NoLogo -NoProfile"),
- writeCommands: ["Write-Output ([string]::Concat('GHOSTTY','_PS_OK'))\r"],
- marker,
- TestContext.Current.CancellationToken);
- }
-
- [Fact(Skip = "ConPTY is Windows-only.", SkipUnless = nameof(IsWindows))]
- public async Task WindowsPowerShellWithProfileProjectsPrompt()
- {
- await AssertGhosttyProjectsConPtyAsync(
- WindowsPowerShellCommand(string.Empty),
- writeCommands: [],
- "PS ",
- TestContext.Current.CancellationToken);
- }
-
- [Fact(Skip = "ConPTY is Windows-only.", SkipUnless = nameof(IsWindows))]
- public async Task ReplayOfPowerShellStartupBytesProjectsIntoGhosttyGrid()
- {
- var marker = "GHOSTTY_REPLAY_OK";
- var captured = await CaptureConPtyBytesAsync(
- WindowsPowerShellCommand("-NoLogo -NoProfile"),
- writeCommands: ["Write-Output ([string]::Concat('GHOSTTY','_REPLAY_OK'))\r", "exit\r"],
- marker,
- TestContext.Current.CancellationToken);
-
- using var ghostty = new GhosttyTerminalEngine(80, 24);
- using var builtIn = new TerminalEngine(80, 24);
- Exception? feedError = null;
- try
- {
- ghostty.Feed(captured);
- builtIn.Feed(captured);
- }
- catch (Exception ex)
- {
- feedError = ex;
- }
-
- AssertProjectionContains(
- marker,
- captured,
- ghostty,
- builtIn,
- feedError,
- responseCount: 0);
- }
-
- private static async Task AssertGhosttyProjectsConPtyAsync(
- string commandLine,
- string[] writeCommands,
- string marker,
- CancellationToken cancellationToken)
- {
- await using var connection = new ConPtyConnection();
- using var ghostty = new GhosttyTerminalEngine(80, 24);
- using var builtIn = new TerminalEngine(80, 24);
- var captured = new List();
- var responses = new List();
- Exception? feedError = null;
- var gate = new object();
-
- ghostty.Invalidated += (_, _) => ghostty.CreateSnapshot(includeHistory: true);
- ghostty.ResponseReady += (_, data) =>
- {
- lock (gate)
- {
- responses.Add(data);
- }
-
- try
- {
- connection.Write(data);
- }
- catch (Exception ex)
- {
- lock (gate)
- {
- feedError ??= ex;
- }
- }
- };
- connection.OutputReceived += (_, data) =>
- {
- try
- {
- lock (gate)
- {
- if (feedError is not null)
- {
- captured.AddRange(data.Span.ToArray());
- return;
- }
- }
-
- ghostty.Feed(data.Span);
- builtIn.Feed(data.Span);
- }
- catch (Exception ex)
- {
- lock (gate)
- {
- feedError ??= ex;
- }
- }
-
- lock (gate)
- {
- captured.AddRange(data.Span.ToArray());
- }
- };
-
- await connection.StartAsync(commandLine, null, 80, 24, cancellationToken);
- foreach (var command in writeCommands)
- {
- connection.Write(command);
- }
-
- await WaitForAsync(
- () =>
- {
- lock (gate)
- {
- return Encoding.UTF8.GetString([.. captured]).Contains(marker, StringComparison.Ordinal);
- }
- },
- TimeSpan.FromSeconds(15),
- () =>
- {
- lock (gate)
- {
- return Encoding.UTF8.GetString([.. captured]);
- }
- },
- cancellationToken);
-
- byte[] snapshot;
- lock (captured)
- {
- snapshot = [.. captured];
- }
-
- AssertProjectionContains(
- marker,
- snapshot,
- ghostty,
- builtIn,
- feedError,
- responses.Count);
- }
-
- private static async Task CaptureConPtyBytesAsync(
- string commandLine,
- string[] writeCommands,
- string marker,
- CancellationToken cancellationToken)
- {
- await using var connection = new ConPtyConnection();
- var captured = new List();
- var exited = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- connection.OutputReceived += (_, data) =>
- {
- lock (captured)
- {
- captured.AddRange(data.Span.ToArray());
- }
- };
- connection.Exited += (_, code) => exited.TrySetResult(code);
-
- await connection.StartAsync(commandLine, null, 80, 24, cancellationToken);
- foreach (var command in writeCommands)
- {
- connection.Write(command);
- }
-
- await WaitForAsync(
- () =>
- {
- lock (captured)
- {
- return Encoding.UTF8.GetString([.. captured]).Contains(marker, StringComparison.Ordinal);
- }
- },
- TimeSpan.FromSeconds(15),
- () =>
- {
- lock (captured)
- {
- return Encoding.UTF8.GetString([.. captured]);
- }
- },
- cancellationToken);
- _ = await exited.Task.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);
-
- lock (captured)
- {
- return [.. captured];
- }
- }
-
- private static void AssertProjectionContains(
- string marker,
- byte[] captured,
- GhosttyTerminalEngine ghostty,
- TerminalEngine builtIn,
- Exception? feedError,
- int responseCount)
- {
- var raw = Encoding.UTF8.GetString(captured);
- var ghosttyText = ViewportText(ghostty);
- var builtInText = ViewportText(builtIn);
- var detail =
- $"feedError={feedError}\n" +
- $"responses={responseCount}\n" +
- $"rawChars={raw.Length} rawNonWs={CountNonWhitespace(raw)}\n" +
- $"ghosttyNonWs={CountNonWhitespace(ghosttyText)} builtinNonWs={CountNonWhitespace(builtInText)}\n" +
- $"ghostty={FormatViewport(ghosttyText)}\n" +
- $"builtin={FormatViewport(builtInText)}\n" +
- $"rawHead={FormatRawHead(captured)}";
-
- Assert.True(feedError is null, detail);
- Assert.Contains(marker, raw, StringComparison.Ordinal);
- Assert.True(CountNonWhitespace(ghosttyText) > 0, detail);
- Assert.True(
- ghosttyText.Contains(marker, StringComparison.Ordinal) ||
- Compact(ghosttyText).Contains(Compact(marker), StringComparison.Ordinal),
- detail);
- Assert.True(
- builtInText.Contains(marker, StringComparison.Ordinal) ||
- Compact(builtInText).Contains(Compact(marker), StringComparison.Ordinal),
- detail);
- }
-
- private static async Task WaitForAsync(
- Func condition,
- TimeSpan timeout,
- Func snapshot,
- CancellationToken cancellationToken)
- {
- var deadline = DateTime.UtcNow + timeout;
- while (DateTime.UtcNow < deadline)
- {
- if (condition())
- {
- return;
- }
-
- await Task.Delay(25, cancellationToken);
- }
-
- Assert.Fail($"Timed out waiting for ConPTY output. snapshot={FormatViewport(snapshot())}");
- }
-
- private static string ViewportText(ITerminalEngine engine)
- {
- var snapshot = engine.CreateSnapshot().Buffer;
- var builder = new StringBuilder();
- for (var row = 0; row < snapshot.Rows; row++)
- {
- foreach (var cell in snapshot.Lines[row].Cells)
- {
- builder.Append(cell.Text);
- }
-
- builder.Append('\n');
- }
-
- return builder.ToString();
- }
-
- private static int CountNonWhitespace(string text) =>
- text.Count(static value => !char.IsWhiteSpace(value));
-
- private static string Compact(string text) =>
- string.Concat(text.Where(static value => !char.IsWhiteSpace(value)));
-
- private static string FormatViewport(string text)
- {
- var trimmed = text.TrimEnd();
- if (trimmed.Length > 400)
- {
- trimmed = trimmed[..400] + "…";
- }
-
- return trimmed.Replace("\n", "\\n").Replace("\r", "\\r");
- }
-
- private static string FormatRawHead(byte[] captured)
- {
- var length = Math.Min(captured.Length, 160);
- return Convert.ToHexString(captured.AsSpan(0, length));
- }
-
- private static string EchoCommand(string value)
- {
- var comSpec = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe";
- return $"\"{comSpec}\" /d /s /c \"echo {value}\"";
- }
-
- private static string WindowsPowerShellCommand(string arguments)
- {
- var powershell = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.System),
- "WindowsPowerShell",
- "v1.0",
- "powershell.exe");
- return $"\"{powershell}\" {arguments}";
- }
-}
diff --git a/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyProjectionTests.cs b/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyProjectionTests.cs
deleted file mode 100644
index ef936a3..0000000
--- a/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyProjectionTests.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Text;
-using Devolutions.Terminal.Core;
-using Devolutions.Terminal.Ghostty;
-using Xunit;
-
-namespace Devolutions.Terminal.Ghostty.Tests;
-
-///
-/// Feeding only marks the projection stale, and a later read re-projects just
-/// the rows Ghostty's render state reports dirty. Reading after every chunk
-/// exercises that incremental path; scrolling the viewport to the bottom then
-/// forces a full re-projection, and both must describe the same screen.
-///
-public sealed class GhosttyProjectionTests
-{
- private const string Esc = "\u001b";
-
- public static TheoryData Streams => new()
- {
- { "partial rows", $"one\r\ntwo\r\nthree{Esc}[2;1Hchanged{Esc}[5;3Hx" },
- { "scrolling", string.Concat(Enumerable.Range(0, 30).Select(i => $"line {i:D2} {new string('#', i % 15)}\r\n")) },
- { "region scroll", $"{Esc}[2;5r{Esc}[5;1H" + string.Concat(Enumerable.Range(0, 12).Select(i => $"r{i}\r\n")) + $"{Esc}[r" },
- { "styled wide", $"{Esc}[1;31mred{Esc}[0m \u4E00\u4E00 {Esc}[44mblue{Esc}[0m e\u0301" },
- { "hyperlink and prompt", $"{Esc}]133;A\u0007PS> {Esc}]133;B\u0007{Esc}]8;;https://example.test\u0007link{Esc}]8;;\u0007\r\n" },
- { "alternate screen", $"primary\r\n{Esc}[?1049h{Esc}[Halt{Esc}[3;3Hz{Esc}[?1049lback" },
- { "erase", $"abcdef\r\nghijkl{Esc}[1;3H{Esc}[K{Esc}[2;2H{Esc}[1P" },
- };
-
- [Theory]
- [MemberData(nameof(Streams))]
- public void IncrementalProjectionMatchesFullProjection(string name, string stream)
- {
- var bytes = Encoding.UTF8.GetBytes(stream);
- using var engine = new GhosttyTerminalEngine(16, 6, historySize: 20);
- var random = new Random(name.Length);
- for (var offset = 0; offset < bytes.Length;)
- {
- var length = Math.Min(bytes.Length - offset, random.Next(1, 9));
- engine.Feed(bytes.AsSpan(offset, length));
- offset += length;
- _ = engine.CreateSnapshot();
- }
-
- var incremental = engine.CreateSnapshot();
- engine.SetScrollOffset(0);
- var full = engine.CreateSnapshot();
-
- AssertSameScreen(name, full, incremental);
- }
-
- [Fact]
- public void FeedDefersProjectionUntilRead()
- {
- using var reader = new GhosttyTerminalEngine(20, 4);
- using var writer = new GhosttyTerminalEngine(20, 4);
- foreach (var line in new[] { "first\r\n", "second\r\n", "third" })
- {
- reader.Feed(line);
- _ = reader.CreateSnapshot();
- writer.Feed(line);
- }
-
- // The cursor and grid accessors project on demand.
- Assert.Equal((reader.CursorX, reader.CursorY), (writer.CursorX, writer.CursorY));
- AssertSameScreen("deferred", reader.CreateSnapshot(), writer.CreateSnapshot());
- Assert.Equal("third", writer.Buffer.GetText(0, 2, 19, 2).TrimEnd());
- }
-
- private static void AssertSameScreen(string name, TerminalSnapshot expected, TerminalSnapshot actual)
- {
- Assert.True(
- (expected.Buffer.CursorX, expected.Buffer.CursorY) == (actual.Buffer.CursorX, actual.Buffer.CursorY),
- $"{name}: cursor");
- Assert.Equal(expected.Buffer.Lines.Count, actual.Buffer.Lines.Count);
- for (var row = 0; row < expected.Buffer.Lines.Count; row++)
- {
- var left = expected.Buffer.Lines[row];
- var right = actual.Buffer.Lines[row];
- Assert.True(left.Wrapped == right.Wrapped, $"{name}: row {row} wrapped");
- Assert.True(
- left.Marks.Select(static mark => mark.StartColumn).SequenceEqual(right.Marks.Select(static mark => mark.StartColumn)),
- $"{name}: row {row} marks");
- for (var column = 0; column < left.Cells.Count; column++)
- {
- Assert.True(
- left.Cells[column].Equals(right.Cells[column]),
- $"{name}: row {row} column {column}: '{left.Cells[column].Text}' vs '{right.Cells[column].Text}'");
- }
- }
- }
-}
diff --git a/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyTerminalEngineTests.cs b/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyTerminalEngineTests.cs
deleted file mode 100644
index 69633ef..0000000
--- a/tests/Devolutions.Terminal.Ghostty.Tests/GhosttyTerminalEngineTests.cs
+++ /dev/null
@@ -1,584 +0,0 @@
-using System.Text;
-using Devolutions.Terminal.Core;
-using Devolutions.Terminal;
-using Devolutions.Terminal.Ghostty;
-using Avalonia.Input;
-using Xunit;
-
-namespace Devolutions.Terminal.Ghostty.Tests;
-
-public sealed class GhosttyTerminalEngineTests
-{
- public static TheoryData SharedVtCorpus => new()
- {
- "plain text",
- "line one\r\nline two",
- "\u001b[2;4Hpositioned",
- "\u001b[31mred\u001b[0m normal",
- "\u001b[1mbold\u001b[0m \u001b[4munderline\u001b[0m",
- "\u001b[?2004hbracketed",
- "\u001b[?1049halt screen\u001b[?1049lprimary",
- };
-
- [Fact]
- public void AbiManifestMatchesPinnedSchema()
- {
- GhosttyAbi.Validate();
-
- Assert.Contains("\"schema\":1", GhosttyAbi.TypeManifest);
- }
-
- [Fact]
- public void AbiCapabilitiesExplicitlyExcludeUnavailableSharedResources()
- {
- using var engine = new GhosttyTerminalEngine();
-
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.UnicodeGraphemeClusters));
- Assert.True(engine.Capabilities.HasFlag(TerminalEngineCapabilities.Vt52Keyboard));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.RowRendition));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.DrcsGlyphs));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.KittyKeyboard));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.ModifyOtherKeys));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.Win32Input));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.SixelImages));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.Iterm2Images));
- Assert.False(engine.Capabilities.HasFlag(TerminalEngineCapabilities.ConEmuImages));
- Assert.Empty(engine.CreateSnapshot().Images);
- Assert.Empty(engine.CreateSnapshot().DrcsGlyphs);
- }
-
- [Fact]
- public void UnsupportedImageProtocolsProduceDeterministicDiagnostics()
- {
- using var engine = new GhosttyTerminalEngine();
- var diagnostics = new List();
- engine.Diagnostic += (_, value) => diagnostics.Add(value);
-
- engine.Feed("\u001bP7q~\u001b\\");
- engine.Feed("\u001b]133");
- engine.Feed("7;File=inline=1:AQ==\u0007");
- engine.Feed("\u001b]9;4;st=0;sz=1;AQ==\u001b\\");
-
- Assert.Equal(
- [
- "image.sixel.unsupported",
- "image.osc1337.unsupported",
- "image.conemu.unsupported",
- ],
- diagnostics.Select(static value => value.Code));
- Assert.Empty(engine.CreateSnapshot().Images);
- }
-
- [Fact]
- public void NonImageDcsQueriesDoNotProduceImageDiagnostics()
- {
- using var engine = new GhosttyTerminalEngine();
- var diagnostics = new List();
- engine.Diagnostic += (_, value) => diagnostics.Add(value);
-
- engine.Feed("\u001bP$qm\u001b\\\u001bP+q544e\u001b\\");
-
- Assert.Empty(diagnostics);
- }
-
- [Fact]
- public void KittyGraphicsProducesSingleUnsupportedDiagnostic()
- {
- using var engine = new GhosttyTerminalEngine();
- var diagnostics = new List();
- engine.Diagnostic += (_, value) => diagnostics.Add(value);
-
- // Chunked kitty transmissions repeat APC G per chunk; only one diagnostic.
- engine.Feed("\u001b_Ga=T,f=32,s=1,v=1,i=1,m=1;AQID\u001b\\");
- engine.Feed("\u001b_Gi=1,m=0;BA==\u001b\\");
-
- Assert.Equal(
- ["image.kitty.unsupported"],
- diagnostics.Select(static value => value.Code));
- }
-
- [Fact]
- public void EngineSurvivesAdversarialCorpusOnSmallStackThread()
- {
- // winterm-ghostty lesson: full libghostty assumed a 16 MB stack and
- // overflowed the host default in production. libghostty-vt is VT-only, but
- // pin the headroom: the engine must survive an adversarial corpus on a
- // 256 KB-stack thread — 4x smaller than the ~1 MB default of the .NET
- // thread-pool threads the ConPTY ReadLoop feeds it from.
- var corpus = BuildAdversarialCorpus();
- Exception? failure = null;
- var thread = new Thread(() =>
- {
- try
- {
- using var engine = new GhosttyTerminalEngine();
- engine.Feed(corpus);
- _ = engine.CreateSnapshot(includeHistory: true);
- _ = engine.Title;
- }
- catch (Exception ex)
- {
- failure = ex;
- }
- }, maxStackSize: 256 * 1024);
- thread.Start();
- thread.Join();
-
- Assert.Null(failure);
- }
-
- private static string BuildAdversarialCorpus()
- {
- var builder = new StringBuilder(capacity: 512 * 1024);
- builder.Append(new string('x', 100 * 1024)); // single over-long line: wrap/reflow
- builder.Append("\r\n");
- builder.Append("\u001b]8;;").Append(new string('a', 64 * 1024)).Append("link\u001b]8;;\u0007\r\n"); // long OSC 8
- for (var i = 0; i < 200; i++)
- {
- builder.Append("\u001b[38;5;").Append(i % 256).Append('m'); // SGR churn
- }
-
- builder.Append("e").Append(new string('̃', 4096)).Append("\r\n"); // deep grapheme
- builder.Append("\u001bP7q").Append(new string('~', 4096)).Append("\u001b\\\r\n"); // sixel-ish DCS
- builder.Append(new string('進', 16 * 1024)).Append("\r\n"); // wide chars
- for (var i = 0; i < 500; i++)
- {
- builder.Append("\u001b[").Append(i % 40 + 1).Append(';').Append(i % 100 + 1).Append('H');
- builder.Append("cell").Append(i).Append("\r\n");
- }
-
- return builder.ToString();
- }
-
- [Theory]
- [InlineData("\u001b")]
- [InlineData("\u001bP")]
- [InlineData("\u001bPq")]
- [InlineData("\u001bPq\u001b")]
- [InlineData("\u001b]")]
- [InlineData("\u001b]1")]
- [InlineData("\u001b]1337;")]
- [InlineData("\u001b]1337;\u001b")]
- public void CancelledImageProbeStateRecoversForFollowingSequence(string prefix)
- {
- foreach (var cancellation in new[] { '\u0018', '\u001A' })
- {
- using var engine = new GhosttyTerminalEngine();
- var diagnostics = new List();
- engine.Diagnostic += (_, value) => diagnostics.Add(value);
- engine.Feed(prefix);
- diagnostics.Clear();
-
- engine.Feed($"{cancellation}\u001bPq~\u001b\\");
-
- Assert.Equal(
- ["image.sixel.unsupported"],
- diagnostics.Select(static value => value.Code));
- }
- }
-
- [Fact]
- public void Vt52KeyboardEncodingMatchesBuiltInModeState()
- {
- using var builtIn = new TerminalEngine();
- using var ghostty = new GhosttyTerminalEngine();
- builtIn.Feed("\u001b[?2l");
- ghostty.Feed("\u001b[?2l");
-
- var expected = KeyMapper.ToVt(
- Key.Up,
- KeyModifiers.None,
- PhysicalKey.ArrowUp,
- null,
- builtIn.InputMode);
- var actual = KeyMapper.ToVt(
- Key.Up,
- KeyModifiers.None,
- PhysicalKey.ArrowUp,
- null,
- ghostty.InputMode);
-
- Assert.Equal("\u001bA", expected);
- Assert.Equal(expected, actual);
- }
-
- [Fact]
- public void HistorySnapshotFromInvalidatedDoesNotClearViewport()
- {
- using var engine = new GhosttyTerminalEngine(120, 30);
- engine.Invalidated += (_, _) => engine.CreateSnapshot(includeHistory: true);
-
- engine.Feed("hello Ghostty");
-
- Assert.Equal("hello Ghostty", LineText(engine.CreateSnapshot().Buffer, 0));
- }
-
- [Fact]
- public void InvalidatedSubscriberExceptionDoesNotFailFeed()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- engine.Invalidated += (_, _) => throw new InvalidOperationException("scrollbar");
-
- engine.Feed("still projected");
-
- Assert.Equal("still projected", LineText(engine.CreateSnapshot().Buffer, 0));
- }
-
- [Fact]
- public void DefaultKeyboardModeIsAnsiNotVt52()
- {
- using var engine = new GhosttyTerminalEngine();
-
- Assert.True(engine.InputMode.AnsiMode);
- Assert.False(engine.InputMode.Win32InputMode);
- Assert.Equal(0, engine.InputMode.ModifyOtherKeys);
- Assert.Equal(KittyKeyboardFlags.None, engine.InputMode.KittyFlags);
- }
-
- [Fact]
- public void KittyKeyboardFlagsAreProjectedFromHostSequences()
- {
- using var engine = new GhosttyTerminalEngine();
-
- engine.Feed("\u001b[>1u");
- Assert.Equal(KittyKeyboardFlags.DisambiguateEscapeCodes, engine.InputMode.KittyFlags);
-
- engine.Feed("\u001b[=3;2u");
- Assert.Equal(
- KittyKeyboardFlags.DisambiguateEscapeCodes | KittyKeyboardFlags.ReportEventTypes,
- engine.InputMode.KittyFlags);
-
- engine.Feed("\u001b[();
- var bells = 0;
- engine.TitleChanged += (_, title) => titles.Add(title);
- engine.Bell += (_, _) => bells++;
-
- engine.Feed("\u001b]0;Ghostty tab\u0007\u0007");
-
- Assert.Equal("Ghostty tab", engine.Title);
- Assert.Contains("Ghostty tab", titles);
- Assert.Equal(1, bells);
- }
-
- [Fact]
- public void ScrollbackCanBeNavigatedAndSnapshotted()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- engine.Feed("one\r\ntwo\r\nthree\r\nfour\r\nfive");
-
- Assert.True(engine.HistoryCount >= 2);
- engine.SetScrollOffset(engine.HistoryCount);
-
- Assert.Equal(engine.HistoryCount, engine.ScrollOffset);
- var snapshot = engine.CreateSnapshot(includeHistory: true).Buffer;
- Assert.Equal(engine.HistoryCount, snapshot.HistoryCount);
- Assert.Contains(snapshot.Lines, line =>
- string.Concat(line.Cells.Select(static cell => cell.Text)).TrimEnd() == "one");
- }
-
- [Fact]
- public void NativeScrollbackHonorsProfileLimit()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 0);
- engine.Feed("one\r\ntwo\r\nthree\r\nfour\r\nfive");
-
- Assert.Equal(0, engine.HistoryCount);
- }
-
- [Fact]
- public void ZeroScrollbackInvalidatesCoordinatesWhenViewportRowsAreDiscarded()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 0);
- var version = engine.Buffer.CoordinateVersion;
-
- engine.Feed("one\r\ntwo\r\nthree\r\nfour");
-
- Assert.True(engine.Buffer.CoordinateVersion > version);
- }
-
- [Fact]
- public void ProjectionPreservesBlankBackgroundAndWrappedRows()
- {
- using var engine = new GhosttyTerminalEngine(5, 3);
- var coordinateVersion = engine.Buffer.CoordinateVersion;
-
- engine.Feed("\u001b[41m\u001b[2J123456");
-
- var snapshot = engine.CreateSnapshot().Buffer;
- Assert.Equal(Devolutions.Terminal.Core.ColorKind.Rgb, snapshot.Lines[0].Cells[4].Attributes.Background.Kind);
- Assert.True(snapshot.Lines[0].Wrapped);
- Assert.Equal(coordinateVersion, engine.Buffer.CoordinateVersion);
- Assert.Equal(Devolutions.Terminal.Core.ShellIntegrationKind.None, snapshot.Lines[2].Cells[4].ShellIntegration);
- }
-
- [Fact]
- public void CursorIsHiddenWhenScrolledOutsideViewport()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- engine.Feed("one\r\ntwo\r\nthree\r\nfour\r\nfive");
-
- engine.SetScrollOffset(engine.HistoryCount);
-
- Assert.False(engine.CursorVisible);
- Assert.Equal(4, engine.CursorX);
- Assert.Equal(2, engine.CursorY);
- }
-
- [Fact]
- public void SearchRevealScrollsTheNativeViewport()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- using var search = new Devolutions.Terminal.TerminalSearchSession(engine);
- engine.Feed("needle\r\ntwo\r\nthree\r\nfour\r\nfive");
-
- search.Update("needle");
-
- Assert.True(engine.ScrollOffset > 0);
- }
-
- [Fact]
- public void PositiveHistoryLimitIsNotConstrainedByGhosttyByteDefault()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 9001);
- engine.Feed(string.Concat(Enumerable.Repeat("line\r\n", 5000)));
-
- Assert.True(engine.HistoryCount > 4000);
- }
-
- [Fact]
- public void SemanticPromptCreatesOneMarkAtItsActualColumn()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- engine.Feed("\u001b]133;A\u0007P> \u001b]133;B\u0007");
-
- var line = engine.CreateSnapshot().Buffer.Lines[0];
-
- var mark = Assert.Single(line.Marks);
- Assert.Equal(0, mark.StartColumn);
- Assert.Equal(Devolutions.Terminal.Core.ShellIntegrationKind.Prompt, line.Cells[0].ShellIntegration);
- }
-
- [Fact]
- public void HistoryEvictionAdvancesCoordinateVersion()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 10);
- engine.Feed(string.Concat(Enumerable.Repeat("line\r\n", 5000)));
- var version = engine.Buffer.CoordinateVersion;
- var history = engine.HistoryCount;
-
- engine.Feed(new string('x', 100_000));
-
- Assert.True(engine.Buffer.CoordinateVersion > version);
- }
-
- [Fact]
- public void Osc52ClipboardWriteHonorsPolicy()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- string? clipboard = null;
- engine.ClipboardWriteRequested += (_, text) => clipboard = text;
- var payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("hello Ω"));
-
- engine.Feed($"\u001b]52;c;{payload}\u0007");
- Assert.Null(clipboard);
-
- engine.ConfigureOptionalFeatures(allowClipboardWrite: true, allowNotifications: false);
- engine.Feed($"\u001b]52;c;{payload}\u0007");
- Assert.Equal("hello Ω", clipboard);
- }
-
- [Fact]
- public void EmptyOsc52RequestsClipboardClear()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- string? clipboard = null;
- engine.ConfigureOptionalFeatures(allowClipboardWrite: true, allowNotifications: false);
- engine.ClipboardWriteRequested += (_, text) => clipboard = text;
-
- engine.Feed("\u001b]52;c;\u0007");
-
- Assert.Equal(string.Empty, clipboard);
- }
-
- [Fact]
- public void DesktopNotificationHonorsPolicy()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- Devolutions.Terminal.Core.TerminalNotification? notification = null;
- engine.NotificationRequested += (_, value) => notification = value;
-
- engine.Feed("\u001b]777;notify;Deploy;Completed\u0007");
- Assert.Null(notification);
-
- engine.ConfigureOptionalFeatures(allowClipboardWrite: false, allowNotifications: true);
- engine.Feed("\u001b]777;notify;Deploy;Completed\u0007");
- Assert.Equal("Deploy", notification?.Title);
- Assert.Equal("Completed", notification?.Body);
- }
-
- [Fact]
- public void HyperlinksAreProjectedIntoSharedCells()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
-
- engine.Feed("\u001b]8;;https://example.com\u001b\\link\u001b]8;;\u001b\\");
-
- var firstCell = engine.CreateSnapshot().Buffer.Lines[0].Cells[0];
- Assert.Equal("https://example.com", firstCell.HyperlinkUri);
- }
-
- [Fact]
- public void DeviceReportsAreReturnedToThePty()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- byte[]? response = null;
- engine.ResponseReady += (_, value) => response = value;
-
- engine.Feed("\u001b[2;4H\u001b[6n");
-
- Assert.Equal("\u001b[2;4R", System.Text.Encoding.ASCII.GetString(response!));
- }
-
- [Fact]
- public void SizeReportsUseCurrentCellPixelGeometry()
- {
- using var engine = new GhosttyTerminalEngine(10, 4);
- var responses = new List();
- engine.ResponseReady += (_, value) =>
- responses.Add(System.Text.Encoding.ASCII.GetString(value));
- engine.Resize(10, 4, 8, 16);
-
- engine.Feed("\u001b[14t\u001b[16t\u001b[18t");
-
- Assert.Contains("\u001b[4;64;80t", responses);
- Assert.Contains("\u001b[6;16;8t", responses);
- Assert.Contains("\u001b[8;4;10t", responses);
- }
-
- [Fact]
- public void PlainOutputDoesNotEmitFalseMetadataChanges()
- {
- using var engine = new GhosttyTerminalEngine(20, 3);
- var titleChanges = 0;
- var workingDirectoryChanges = 0;
- engine.TitleChanged += (_, _) => titleChanges++;
- engine.WorkingDirectoryChanged += (_, _) => workingDirectoryChanges++;
-
- engine.Feed("ordinary output");
-
- Assert.Equal(0, titleChanges);
- Assert.Equal(0, workingDirectoryChanges);
- }
-
- [Fact]
- public void FirstLargeFeedEvictionAdvancesCoordinateVersion()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 10);
- var version = engine.Buffer.CoordinateVersion;
-
- engine.Feed(new string('x', 100_000));
-
- Assert.True(engine.Buffer.CoordinateVersion > version);
- }
-
- [Fact]
- public void FirstControlSequenceScrollEvictionAdvancesCoordinateVersion()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 10);
- var version = engine.Buffer.CoordinateVersion;
-
- engine.Feed(string.Concat(Enumerable.Repeat("\u001bD", 5000)));
-
- Assert.True(engine.Buffer.CoordinateVersion > version);
- }
-
- [Fact]
- public void FirstLineFeedEvictionAtLimitAdvancesCoordinateVersion()
- {
- using var engine = new GhosttyTerminalEngine(20, 3, historySize: 5000);
- var version = engine.Buffer.CoordinateVersion;
-
- engine.Feed(string.Concat(Enumerable.Repeat("\n", 10_000)));
-
- Assert.True(engine.Buffer.CoordinateVersion > version);
- }
-
- [Theory]
- [MemberData(nameof(SharedVtCorpus))]
- public void SharedCorpusMatchesBuiltInProjection(string sequence)
- {
- using var builtIn = new Devolutions.Terminal.Core.TerminalEngine(40, 5);
- using var ghostty = new GhosttyTerminalEngine(40, 5);
-
- builtIn.Feed(sequence);
- ghostty.Feed(sequence);
-
- var expected = builtIn.CreateSnapshot().Buffer;
- var actual = ghostty.CreateSnapshot().Buffer;
- Assert.Equal(expected.CursorX, actual.CursorX);
- Assert.Equal(expected.CursorY, actual.CursorY);
- Assert.Equal(
- Enumerable.Range(0, expected.Rows).Select(row => LineText(expected, row)),
- Enumerable.Range(0, actual.Rows).Select(row => LineText(actual, row)));
- Assert.Equal(builtIn.BracketedPaste, ghostty.BracketedPaste);
- Assert.Equal(builtIn.AlternateBufferActive, ghostty.AlternateBufferActive);
- }
-
- private static string LineText(Devolutions.Terminal.Core.TextBufferSnapshot snapshot, int row) =>
- string.Concat(snapshot.Lines[row].Cells.Select(static cell => cell.Text)).TrimEnd();
-}
diff --git a/tests/Devolutions.Terminal.Settings.Tests/SettingsParityTests.cs b/tests/Devolutions.Terminal.Settings.Tests/SettingsParityTests.cs
index a25b45c..5295f94 100644
--- a/tests/Devolutions.Terminal.Settings.Tests/SettingsParityTests.cs
+++ b/tests/Devolutions.Terminal.Settings.Tests/SettingsParityTests.cs
@@ -474,8 +474,10 @@ public void ParsesCopyFormattingCompatibilityForms(string json, CopyFormat expec
}
[Fact]
- public void TerminalEngineCanBeSelectedGloballyAndPerProfile()
+ public void RetiredTerminalEngineSettingLoadsAndIsLeftInPlace()
{
+ // The Ghostty engine was removed; older files that still select it must load
+ // cleanly, and saving must not rewrite a key the user wrote.
const string user = """
{
"experimental.terminalEngine": "ghostty",
@@ -484,7 +486,7 @@ public void TerminalEngineCanBeSelectedGloballyAndPerProfile()
{
"guid": "{11111111-1111-1111-1111-111111111111}",
"name": "Inbox",
- "experimental.terminalEngine": "builtin"
+ "experimental.terminalEngine": "ghostty"
}
]
}
@@ -493,36 +495,10 @@ public void TerminalEngineCanBeSelectedGloballyAndPerProfile()
var settings = SettingsLoader.Load(Defaults, user);
- Assert.Equal(TerminalEngineKind.Ghostty, settings.TerminalEngine);
- Assert.Equal(TerminalEngineKind.BuiltIn, Assert.Single(settings.Profiles).TerminalEngine);
-
+ Assert.DoesNotContain(settings.Diagnostics, d => d.Message.Contains("terminalEngine", StringComparison.Ordinal));
+ Assert.Equal("Inbox", Assert.Single(settings.Profiles).Name);
var serialized = SettingsLoader.SerializeUserDocument(settings);
Assert.Contains("\"experimental.terminalEngine\": \"ghostty\"", serialized);
- Assert.Contains("\"experimental.terminalEngine\": \"builtin\"", serialized);
- }
-
- [Fact]
- public void InheritedTerminalEngineRemovesProfileOverride()
- {
- const string user = """
- {
- "profiles": {
- "list": [
- {
- "guid": "{11111111-1111-1111-1111-111111111111}",
- "name": "Inbox",
- "experimental.terminalEngine": "ghostty"
- }
- ]
- }
- }
- """;
- var settings = SettingsLoader.Load(Defaults, user);
- Assert.Single(settings.Profiles).TerminalEngine = null;
-
- var serialized = SettingsLoader.SerializeUserDocument(settings);
-
- Assert.DoesNotContain("\"experimental.terminalEngine\": \"ghostty\"", serialized);
}
[Fact]
diff --git a/tools/Devolutions.Terminal.Bench/Devolutions.Terminal.Bench.csproj b/tools/Devolutions.Terminal.Bench/Devolutions.Terminal.Bench.csproj
index bb18397..c43046a 100644
--- a/tools/Devolutions.Terminal.Bench/Devolutions.Terminal.Bench.csproj
+++ b/tools/Devolutions.Terminal.Bench/Devolutions.Terminal.Bench.csproj
@@ -7,7 +7,6 @@
falseenablefalse
- true
@@ -16,9 +15,7 @@
-
-
diff --git a/tools/Devolutions.Terminal.Bench/Program.cs b/tools/Devolutions.Terminal.Bench/Program.cs
index bb4d84b..02f70b9 100644
--- a/tools/Devolutions.Terminal.Bench/Program.cs
+++ b/tools/Devolutions.Terminal.Bench/Program.cs
@@ -22,12 +22,12 @@ namespace Devolutions.Terminal.Bench;
/// coalescing ratio).
/// render — snapshot/planning and CPU Skia rasterization, measured separately
/// for a static screen, one-row updates, and scrolling screens.
-/// corpus — write the deterministic VT input for reuse with Ghostty benchmarks.
-/// vt — same-process ingest comparison of the builtin engine against raw
-/// libghostty-vt and DT's Ghostty engine over per-workload corpora.
+/// corpus — write the deterministic VT input for reuse with other benchmarks.
+/// vt — same-process ingest throughput of the builtin engine against an
+/// in-run clock control over per-workload corpora.
///
/// Caveat: control mode does not paint. Render mode uses a raster surface, not
-/// Avalonia's GPU/compositor or Ghostty's OpenGL/Metal renderer.
+/// Avalonia's GPU/compositor.
///
internal static class Program
{
diff --git a/tools/Devolutions.Terminal.Bench/VtBench.cs b/tools/Devolutions.Terminal.Bench/VtBench.cs
index 7da7156..6d3a5a5 100644
--- a/tools/Devolutions.Terminal.Bench/VtBench.cs
+++ b/tools/Devolutions.Terminal.Bench/VtBench.cs
@@ -5,28 +5,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Devolutions.Terminal.Core;
-using Devolutions.Terminal.Ghostty;
namespace Devolutions.Terminal.Bench;
///
-/// Same-process, same-bytes VT ingest comparison between the builtin engine and
-/// Ghostty. Three targets consume identical chunks:
-/// builtin — DT's own (the default engine).
-/// ghostty-vt — raw ghostty_terminal_vt_write: Ghostty's parser and
-/// screen with nothing else, the bar the builtin engine must clear.
-/// ghostty — DT's , i.e. ghostty-vt plus
-/// the per-feed render-state projection DT needs to paint it.
+/// Same-process, same-bytes VT ingest throughput of the builtin engine, measured
+/// against an in-run control. Two targets consume identical chunks:
+/// builtin — DT's .
+/// control — FNV-1a over the fed bytes: a serial loop that does no terminal
+/// work, so its speed follows only the core clock and load.
/// Runs interleave the targets in rotating order so thermal drift and
-/// background load hit every target alike; fresh engines are built outside the
-/// timed region. A verdict is only "faster"/"slower" when the run ranges do
-/// not overlap.
+/// background load hit both alike; fresh engines are built outside the timed
+/// region. builtin/control is the number to compare across sessions, because
+/// absolute MB/s drifts with the CPU's turbo state.
///
-internal static partial class VtBench
+internal static class VtBench
{
public const string Builtin = "builtin";
- public const string GhosttyVt = "ghostty-vt";
- public const string GhosttyDt = "ghostty";
+ public const string Control = "control";
public static int Run(string[] args)
{
@@ -40,7 +36,7 @@ public static int Run(string[] args)
var columns = Program.GetOption(args, "--columns", 120);
var rows = Program.GetOption(args, "--rows", 30);
var workloads = Split(Program.GetStringOption(args, "--workloads"), VtWorkloads.Names);
- var targets = Split(Program.GetStringOption(args, "--targets"), [Builtin, GhosttyVt, GhosttyDt]);
+ var targets = Split(Program.GetStringOption(args, "--targets"), [Builtin, Control]);
var exportDirectory = Program.GetStringOption(args, "--export");
var jsonPath = Program.GetStringOption(args, "--json");
var dataPath = Program.GetStringOption(args, "--data");
@@ -52,9 +48,9 @@ public static int Run(string[] args)
foreach (var target in targets)
{
- if (target is not (Builtin or GhosttyVt or GhosttyDt))
+ if (target is not (Builtin or Control))
{
- throw new ArgumentException($"unknown target '{target}' (expected {Builtin}|{GhosttyVt}|{GhosttyDt})");
+ throw new ArgumentException($"unknown target '{target}' (expected {Builtin}|{Control})");
}
}
@@ -95,19 +91,11 @@ public static int Run(string[] args)
frameBytes,
runs,
[]);
- var exitCode = 0;
foreach (var (name, bytes) in inputs)
{
var sha = Convert.ToHexString(SHA256.HashData(bytes));
Console.WriteLine();
Console.WriteLine($"[{name}] {bytes.Length / (1024.0 * 1024):F1} MiB sha256={sha[..16]}… {VtWorkloads.Describe(name)}");
- var equivalence = targets.Contains(Builtin) && (targets.Contains(GhosttyVt) || targets.Contains(GhosttyDt))
- ? CheckEquivalence(bytes, columns, rows, chunkSize)
- : null;
- if (equivalence is not null)
- {
- Console.WriteLine($" screen equivalence vs ghostty: {equivalence}");
- }
var samples = targets.ToDictionary(target => target, _ => new List());
for (var run = -warmup; run < runs; run++)
@@ -134,24 +122,16 @@ public static int Run(string[] args)
$"retained={result.RetainedMiB:F1} MiB");
}
- string? verdict = null;
double? ratio = null;
var builtin = results.FirstOrDefault(result => result.Target == Builtin);
- var reference = results.FirstOrDefault(result => result.Target == GhosttyVt);
- if (builtin is not null && reference is not null)
+ var control = results.FirstOrDefault(result => result.Target == Control);
+ if (builtin is not null && control is not null)
{
- ratio = builtin.MedianMBps / reference.MedianMBps;
- verdict = builtin.MinMBps > reference.MaxMBps ? "faster"
- : builtin.MaxMBps < reference.MinMBps ? "slower"
- : "overlap";
- Console.WriteLine($" builtin vs ghostty-vt: {ratio:F2}x median throughput, verdict={verdict}");
- if (verdict != "faster")
- {
- exitCode = 1;
- }
+ ratio = builtin.MedianMBps / control.MedianMBps;
+ Console.WriteLine($" builtin / control: {ratio:F4} median throughput");
}
- report.Workloads.Add(new VtWorkloadReport(name, bytes.Length, sha, equivalence, results, ratio, verdict));
+ report.Workloads.Add(new VtWorkloadReport(name, bytes.Length, sha, results, ratio));
}
if (jsonPath is not null)
@@ -161,9 +141,7 @@ public static int Run(string[] args)
Console.WriteLine($"wrote {jsonPath}");
}
- // A non-zero exit means "builtin did not beat ghostty-vt on every workload";
- // scripts use it as a gate, so --no-gate keeps a measurement-only run green.
- return Array.IndexOf(args, "--no-gate") >= 0 ? 0 : exitCode;
+ return 0;
}
private static readonly JsonSerializerOptions JsonOptions = new()
@@ -244,52 +222,10 @@ private static long PrivateBytes()
private static IVtTarget Create(string target, int columns, int rows) => target switch
{
Builtin => new EngineTarget(new TerminalEngine(columns, rows)),
- GhosttyDt => new EngineTarget(new GhosttyTerminalEngine(columns, rows)),
- GhosttyVt => new RawGhosttyTarget(columns, rows),
+ Control => new ControlTarget(),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
};
- ///
- /// Throughput is only comparable if both engines did the same work, so the
- /// final visible screen and cursor must match. A mismatch is reported (with the
- /// first differing row) rather than failing, because some workloads hit
- /// documented engine differences such as grapheme-width policy.
- ///
- private static string CheckEquivalence(byte[] bytes, int columns, int rows, int chunkSize)
- {
- using var builtin = new TerminalEngine(columns, rows);
- using var ghostty = new GhosttyTerminalEngine(columns, rows);
- for (var offset = 0; offset < bytes.Length; offset += chunkSize)
- {
- var chunk = bytes.AsSpan(offset, Math.Min(chunkSize, bytes.Length - offset));
- builtin.Feed(chunk);
- ghostty.Feed(chunk);
- }
-
- var differing = 0;
- var first = -1;
- for (var row = 0; row < rows; row++)
- {
- var expected = builtin.Buffer.GetText(0, row, columns - 1, row).TrimEnd();
- var actual = ghostty.Buffer.GetText(0, row, columns - 1, row).TrimEnd();
- if (!string.Equals(expected, actual, StringComparison.Ordinal))
- {
- differing++;
- first = first < 0 ? row : first;
- }
- }
-
- var cursor = builtin.CursorX == ghostty.CursorX && builtin.CursorY == ghostty.CursorY
- ? "cursor equal"
- : $"cursor builtin=({builtin.CursorX},{builtin.CursorY}) ghostty=({ghostty.CursorX},{ghostty.CursorY})";
- var history = builtin.HistoryCount == ghostty.HistoryCount
- ? $"history equal ({builtin.HistoryCount})"
- : $"history builtin={builtin.HistoryCount} ghostty={ghostty.HistoryCount}";
- return differing == 0
- ? $"identical text, {cursor}, {history}"
- : $"MISMATCH {differing}/{rows} rows (first row {first}), {cursor}, {history}";
- }
-
private static VtTargetReport Summarize(string target, List samples)
{
static double Mebibytes(VtSample sample) => sample.Bytes / (1024.0 * 1024.0);
@@ -333,102 +269,30 @@ private sealed class EngineTarget(ITerminalEngine engine) : IVtTarget
}
///
- /// The pinned libghostty-vt with only the scrollback limit configured to match
- /// the builtin engine's 9,001 lines; no effects or projection. A frame is what
- /// Ghostty's renderer does per frame: update its render state, then clean it.
+ /// FNV-1a over every fed byte. Each step depends on the previous one, so the
+ /// loop cannot be vectorized and its speed tracks the core clock rather than
+ /// memory bandwidth, which is what drifts between sessions.
///
- private sealed unsafe partial class RawGhosttyTarget : IVtTarget
+ private sealed class ControlTarget : IVtTarget
{
- private const int ScrollbackBytesOption = 27;
- private const int ScrollbackLinesOption = 28;
- private nint _terminal;
- private nint _renderState;
-
- public RawGhosttyTarget(int columns, int rows)
- {
- Check(TerminalNew(0, out _terminal, checked((ushort)columns), checked((ushort)rows)), "ghostty_terminal_new");
- try
- {
- Check(TerminalSet(_terminal, ScrollbackBytesOption, null), "remove scrollback byte limit");
- var lines = (nuint)9001;
- Check(TerminalSet(_terminal, ScrollbackLinesOption, &lines), "set scrollback line limit");
- Check(RenderStateNew(0, out _renderState), "ghostty_render_state_new");
- }
- catch
- {
- Dispose();
- throw;
- }
- }
+ private uint _hash = 2166136261;
public void Feed(ReadOnlySpan data)
{
- fixed (byte* bytes = data)
+ var hash = _hash;
+ foreach (var value in data)
{
- TerminalWrite(_terminal, bytes, (nuint)data.Length);
+ hash = (hash ^ value) * 16777619;
}
- }
-
- public void Frame()
- {
- Check(RenderStateUpdate(_renderState, _terminal), "ghostty_render_state_update");
- Check(RenderStateClean(_renderState), "ghostty_render_state_clean");
- }
- public void Dispose()
- {
- if (_renderState != 0)
- {
- RenderStateFree(_renderState);
- _renderState = 0;
- }
-
- if (_terminal != 0)
- {
- TerminalFree(_terminal);
- _terminal = 0;
- }
+ _hash = hash;
}
- private static void Check(int result, string operation)
+ public void Frame()
{
- if (result != 0)
- {
- throw new InvalidOperationException($"{operation} failed with {result}");
- }
}
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_terminal_new")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial int TerminalNew(nint allocator, out nint terminal, ushort columns, ushort rows);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_terminal_set")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial int TerminalSet(nint terminal, int option, void* value);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_terminal_vt_write")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial void TerminalWrite(nint terminal, byte* bytes, nuint length);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_terminal_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial void TerminalFree(nint terminal);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_render_state_new")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial int RenderStateNew(nint allocator, out nint state);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_render_state_update")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial int RenderStateUpdate(nint state, nint terminal);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_render_state_clean")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial int RenderStateClean(nint state);
-
- [LibraryImport("ghostty-vt", EntryPoint = "ghostty_render_state_free")]
- [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
- private static partial void RenderStateFree(nint state);
+ public void Dispose() => GC.KeepAlive(_hash);
}
private readonly record struct VtSample(
@@ -458,10 +322,8 @@ private sealed record VtWorkloadReport(
string Name,
long Bytes,
string Sha256,
- string? Equivalence,
VtTargetReport[] Targets,
- double? BuiltinVsGhosttyVt,
- string? Verdict);
+ double? BuiltinVsControl);
private sealed record VtTargetReport(
string Target,
diff --git a/tools/Devolutions.Terminal.Bench/VtWorkloads.cs b/tools/Devolutions.Terminal.Bench/VtWorkloads.cs
index 82ce6dd..9a0a393 100644
--- a/tools/Devolutions.Terminal.Bench/VtWorkloads.cs
+++ b/tools/Devolutions.Terminal.Bench/VtWorkloads.cs
@@ -4,7 +4,7 @@ namespace Devolutions.Terminal.Bench;
///
/// Deterministic VT byte streams for engine-ingest comparisons. Each workload
-/// isolates one kind of terminal work so a regression or a gap against Ghostty
+/// isolates one kind of terminal work so a regression
/// points at a code path instead of an average. The shapes follow
/// alacritty/vtebench (dense/light cells, cursor motion, scrolling regions,
/// unicode) and Ghostty's own `ghostty-gen` generators (+ascii, +utf8, +osc).
@@ -186,9 +186,9 @@ private static byte[] Utf8(int targetBytes, int columns)
{
if (!started)
{
- // DECSET 2027 (grapheme clustering): without it Ghostty measures a ZWJ
- // sequence per codepoint while the builtin engine clusters it, and the
- // two screens would wrap differently.
+ // DECSET 2027 (grapheme clustering) keeps ZWJ sequences clustered on
+ // terminals that make clustering opt-in, so the corpus wraps the same
+ // everywhere it is replayed.
writer.Append($"{Esc}[?2027h");
started = true;
}