DEV Community

Chen Yuan
Chen Yuan

Posted on Originally published at dispatch-blog.hashnode.dev

Libghostty 1.0: What a Stable C ABI for Terminal Emulation Actually Promises

Twelve and a half million active users got a new terminal emulator this year, and most of them will never know. They did not download anything, and nobody asked them to switch. JetBrains moved the terminal inside IntelliJ, PyCharm, GoLand, and the rest of its IDEs onto libghostty-vt, the terminal emulation library extracted from the Ghostty project, and the usage count in JetBrains' own reporting alone cleared 12.5 million recurring users before any of the other adopters were counted.

On November 6, Mitchell Hashimoto tagged libghostty-vt 1.0 and published the announcement on his blog. The post reached the Hacker News front page within hours, and the number that matters in it is not a benchmark. It is the word "stable" applied to the C ABI, because that single property is what turns a library from an interesting extraction into something a Linux distribution, an IDE vendor, or a commercial product can build on without reservation. This article reads the release the way you would read a dependency you are about to adopt: what the ABI promise actually says, how it is enforced, what the packaging story looks like, and where the edges of the library are.

From a bug report to a default

Hashimoto frames the adoption story with a neat symmetry. A year earlier, when he first announced that libghostty was coming, he used a bug in JediTerm, the terminal emulator inside JetBrains IDEs, as his example of why terminal emulation is hard to get right. A year later, the terminal in those IDEs runs on the library he built instead. The JediTerm bug was the motivation; the replacement is the answer.

The rest of the adopter list explains why a tagged release mattered now. Neovim has an approved pull request replacing libvterm with libghostty-vt, which among other things closes a ten-year-old issue about scrollback reflowing when the window is resized. The maintainers were explicitly waiting for a tagged release so distributions could package the dependency separately, and 1.0 is the release that unblocks the merge. KDE developers are experimenting with a Konsole replacement on top of it. Vercel's Turborepo uses it to render task output in its TUI, devenv uses it to drive shells, and the testing company Antithesis used it to fuzz TUI programs and turned up real bugs in btop and other tools. The ghostty-web project compiles it to WebAssembly with an Xterm.js-compatible API, and commercial products like OrbStack embed it. A wave of new terminals and agentic coding tools, herdr and cmux among them, were built on the library rather than writing yet another emulator from scratch.

Community bindings exist for Rust, Go, JavaScript, Swift, Python, .NET, Elixir, Dart, and Java, and the awesome-libghostty list tracks well over a hundred projects. Hashimoto's summary is that more people now use a libghostty-powered terminal than use Ghostty the application, by a large multiple. That inversion is the strongest argument that the extraction was the right call, and it raises the stakes on the question the rest of the release answers: what exactly can these consumers rely on?

What the 1.0 label actually freezes

The 1.0 announcement is precise about the promise. libghostty-vt follows semantic versioning, and it carries a strict ABI compatibility commitment aimed at two audiences: packagers who need to ship the library through official distribution tooling, and program developers who need to dynamically link against it without fearing the next upgrade.

ABI stability is a stronger claim than API stability, and the difference matters in practice. API stability means your source code still compiles against the new version. ABI stability means the binary you already compiled keeps working when the shared library on the system is swapped for a newer one. A distribution can upgrade libghostty-vt to fix a parsing bug, and every program linked against it, from a KDE terminal prototype to an editor you installed last year, keeps running without a rebuild. That property is what lets a library become infrastructure rather than a vendored copy inside each consumer's repository.

Getting there is mostly a matter of refusing a set of tempting designs, and the announcement lists the constraints the library accepted to keep the promise.

Opaque handles and enum-keyed setters

The design rule set is short enough to quote in full. Every object in the API is an opaque handle, so the layout of no structure is part of the contract. The hundreds of values a program might read or write go through a small set of enum-keyed getters and setters, which means new values can be added by extending an enum rather than by exporting a new symbol. A program compiled with newer headers but running against an older library receives a GHOSTTY_UNSUPPORTED result instead of a crash or undefined behavior. The few non-opaque structs that exist carry their own size so fields can be appended later, or hold reserved padding for the same purpose. Enums are pinned to int on every compiler, as far as the C standard allows.

You can see the pattern directly in the public header. Creating and driving a terminal looks like this:

#include <ghostty/vt.h>
#include <string.h>

GhosttyTerminal terminal;
ghostty_terminal_new(NULL, &terminal, 80, 24);

const char* text = "Hello, \x1b[1;32mworld\x1b[0m!\r\n";
ghostty_terminal_vt_write(terminal, (const uint8_t*)text, strlen(text));

ghostty_terminal_free(terminal);
Enter fullscreen mode Exit fullscreen mode

The configuration side uses the enum-keyed shape the announcement describes. Options are set through one function and one enum, so the surface area of the ABI stays flat as the option set grows:

ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_BELL, bell_config);
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_USERDATA, records);
Enter fullscreen mode Exit fullscreen mode

Contrast this with the more common C library pattern where each new knob is a new exported function. Every exported symbol is a permanent resident of the ABI, and every struct visible in a header is a layout you can never change. The enum-keyed approach trades a slightly less ergonomic call site for the freedom to extend the library for years without breaking the contract. It is the same family of technique that long-lived C libraries have converged on, applied here from the first commit rather than retrofitted after a painful 1.0.

A stability policy that CI enforces

Promises about ABI compatibility have a poor track record when they are enforced by human discipline alone. libghostty-vt's policy is enforced by its continuous integration instead, and the announcement describes three layers.

On Linux, every change is checked with libabigail, the standard ABI comparison tool, against a committed baseline. A change that alters the ABI in an incompatible way fails the build before it merges. On macOS and WebAssembly, the same role is played by golden type manifests. And on ELF platforms, every exported symbol is versioned, so the dynamic loader itself refuses to load a mismatched library by name rather than failing in some obscure way at runtime.

A maintainer reviewing a pull request can run the same comparison locally against two builds:

abidiff libghostty-vt.so.1.0.0 libghostty-vt.so.1.1.0
Enter fullscreen mode Exit fullscreen mode

The interesting property here is not the tooling but the placement. ABI checking sits in the merge path, not in a release checklist, so the policy cannot decay between releases. The golden manifests on the Apple and WASM targets acknowledge that libabigail's ELF focus does not cover those platforms, and the project chose a different mechanism rather than a weaker promise.

Packaging is part of the release

The 1.0 milestone is as much about distribution mechanics as it is about the API. libghostty-vt now has its own release cycle, separate from the Ghostty application, with its own vt-prefixed version tags. Each release publishes a signed source tarball plus a companion dependencies tarball containing every Zig package in Zig's own cache layout, so a distribution can build the library fully offline. Every file ships with a minisig signature, and a signed SHA256SUMS file covers the checksums.

The build installs shared and static libraries with a proper SONAME, headers, pkg-config files, and a CMake package config, and it honors the prefix, DESTDIR, and lib64 conventions that packaging scripts rely on. Debug info is kept by default so distributions can split it into their own debug packages. The whole process is documented in a PACKAGING-LIBGHOSTTY.md file that ships with the tarball, which carries the blunt warning that the document is only accurate for the source alongside it.

The dependency story is tuned for the same audience. The library is MIT licensed with no copyleft anywhere in the tree, and at runtime it needs nothing beyond libc, with a fully freestanding build available for targets that do not even want that. The compiled-in dependencies are simdutf and Google Highway for SIMD, a compact Unicode table for grapheme data, and Wuffs for decoding images in the Kitty graphics protocol, with the SIMD libraries built without libc++ so there is no C++ standard library dependency. Every dependency is listed in a machine-readable manifest with its version, SPDX license, and hash, and a build flag lets a distribution link the system copies of simdutf or Highway instead. That manifest is a quiet gift to distribution maintainers, whose bundled-library metadata requirements are usually met with archaeology.

Portability as a resource contract

The tested-platform matrix covers Linux on glibc and musl, macOS 13 and later, iOS 13 and later, FreeBSD, Windows, Android with the 16 KB page size that Android 15 requires, and freestanding WebAssembly, each with the artifact shape its platform expects: versioned shared libraries on ELF systems, an XCFramework for Apple platforms, a DLL with an import library on Windows, and two WASM builds where the size-optimized one comes in under a megabyte with every feature enabled. A second tier, including bare-metal RISC-V and ARM Thumb, WASI, Windows on ARM64, illumos, and 32-bit x86, builds in CI but is not yet under automated test.

The more interesting portability claim is about resources rather than platforms. The library creates no threads and allocates memory only through an allocator the embedder can customize. Hashimoto reports running ten thousand active terminals, each with a live shell producing output, on a Linux server with four cores and eight gigabytes of RAM, noting that he had to use dash for the test because bash alone would have consumed more memory than the library did and blown the budget. The feature flags deserve a mention too: subsystems like input encoding, search, selection, and Kitty graphics can be compiled out, roughly halving the WASM module, and every function stays exported and simply returns GHOSTTY_UNSUPPORTED, so embedder code does not change between configurations.

What a minimal terminal looks like now

The practical payoff lands in a single sentence of the announcement: you can now build a terminal in about 1,500 lines of C with more features and better parsing throughput than most terminals that existed in 2025. The library holds the emulator state, the screen, scrollback, cursor, styles, and modes; the embedder owns the drawing and the pty. The header's own documentation ships eighteen example programs covering the real integration surfaces, from feeding VT sequences and reading the screen back, to render-state drawing, search, selection gestures, Kitty graphics, and encoding keys and mouse events with the Kitty keyboard protocol.

For anyone consuming it from a higher-level language, the embedding path runs through the same C ABI:

import ctypes

lib = ctypes.CDLL("libghostty-vt.so")
lib.ghostty_terminal_new.restype = ctypes.c_int
Enter fullscreen mode Exit fullscreen mode

That is the whole trick behind the binding ecosystem: one stable C surface, and Rust, Go, Swift, Python, and the rest each wrap it idiomatically.

What the library does not do

The boundaries are stated plainly. libghostty-vt does not draw and does not talk to a pty; rendering and process wiring belong to the embedder. The 1.0 promise attaches to libghostty-vt specifically, not to the whole Ghostty application. The second tier of platforms has known users but no automated testing. And a program built against future headers talking to today's library gets GHOSTTY_UNSUPPORTED, which is honest degradation, not magic.

None of that diminishes the milestone. Terminal emulation has been rewritten per project for decades, accumulating subtly incompatible parsers, and the 1.0 release is a bet that a single, rigorously versioned implementation can end that fragmentation. With an IDE fleet, an editor's approved pull request, and a hundred tracked projects already on board, the bet is off to a credible start.


Originally published on Dispatch.

Top comments (0)