Section 5: A libghostty-Style Reusable Architecture
This is an architectural study, not a build. It is placed last deliberately: the boundaries you are about to defend are conclusions drawn from four sections of experience, and a boundary argument you have not earned is just an opinion with good crate names.
Do not attempt to reproduce Ghostty. The goal is to explore how a terminal implementation can be packaged as a reusable engine — and then to check your own workspace against that standard.
What libghostty Is, and Why It Is Interesting
Ghostty is a terminal emulator written in Zig. Its architecture separates a reusable terminal core — libghostty — from the platform-specific applications built on it: a native Swift/AppKit app on macOS and a GTK app on Linux, both consuming the core through a C ABI.
That structure is the interesting part, and it generalizes far beyond Ghostty:
┌──────────────────────────────────────────────────────────────┐
│ APPLICATIONS (platform-native, written in whatever suits) │
│ macOS: Swift + AppKit Linux: GTK │
└───────────────────────┬──────────────────────────────────────┘
│ C ABI (the stable boundary)
┌───────────────────────▼──────────────────────────────────────┐
│ libghostty — the reusable engine │
│ VT parsing · terminal state · screen · input encoding │
│ PTY/process integration · renderer-independent data │
└──────────────────────────────────────────────────────────────┘
Why draw the line there rather than shipping one monolithic app?
| Reason | Consequence |
|---|---|
| Native UI per platform | A macOS app that feels like a macOS app, without a cross-platform toolkit's compromises |
| One implementation of the hard parts | The VT parser and screen model are written once and shared |
| Testability | The core is testable with no window, no GPU, no display server |
| Embeddability | The same engine can power an IDE panel, a headless simulator, or a test harness |
| Language independence | A C ABI means the UI need not be in the core's language |
Note: Ghostty's internals evolve, and the exact shape of libghostty's public API is a moving target. Do not treat any specific function list in this book as current — check the project. What is stable, and what this section teaches, is the shape of the argument: which concerns separate, and what each separation buys.
Other terminals draw comparable lines. alacritty_terminal is Alacritty's core as a Rust crate.
vte is its parser, reusable by anyone. termwiz is WezTerm's terminal library. The pattern is
industry-wide, and you already built it — Section 5 is where you make it explicit and defend it.
The Nine Concerns
The brief asks for a design that separates these. You have already separated most of them; this section is where you check.
| # | Concern | Your crate |
|---|---|---|
| 1 | Terminal protocol parsing | terminal-protocol |
| 2 | Terminal state and screen buffers | terminal-core |
| 3 | Input encoding | terminal-input |
| 4 | PTY and process integration | terminal-pty |
| 5 | Rendering-independent terminal data | terminal-render-model |
| 6 | Platform-specific integration | terminal-gui (windowing), terminal-pty (OS) |
| 7 | Application UI | terminal-gui |
| 8 | Configuration | (you probably do not have this yet — see below) |
| 9 | Multiplexer logic | terminal-mux |
Concern 8 is the one almost everyone gets wrong, and it is worth its own paragraph. Configuration
tends to become a global that every layer reaches into, which silently couples everything to
everything. The discipline: configuration is data, passed down, never reached up for.
terminal-core takes a TerminalConfig in its constructor; it does not read a global, does not
watch a file, and does not know what TOML is.
The Chapters
| Chapter | Covers |
|---|---|
| Crate Boundaries | Each crate's responsibility, forbidden knowledge, API, state, deps, tests, portability |
| Embedding Scenarios | Six different consumers of one core, and what each proves |
| FFI and Bindings | C ABI, Swift, C++, WebAssembly — after the Rust API is stable |
The Lab
| Lab | Milestone | Build |
|---|---|---|
| Lab 20 | M13 | Stabilize the APIs; prove four consumers; optionally ship a C ABI |
The Ordering Rule
Do not design the FFI before the Rust API is stable.
An FFI is a commitment. Once a C header exists, changing it breaks consumers you cannot see. And an FFI designed against an unstable API bakes in accidents: it exposes internals that were about to change, and it forces the Rust API to keep shapes that only existed for the C bridge.
The correct order:
1. Build it (Sections 1-4).
2. Use it four different ways. ← this is what reveals the real API
3. Stabilize the Rust API.
4. THEN, if you need it, design the C ABI.
5. Language bindings on top of the C ABI.
Step 2 is the load-bearing one. An API used by exactly one consumer is not a boundary; it is a refactoring of one program into two files. Four consumers is the test.
Deliverables
-
terminal-protocolandterminal-corecompile forwasm32-unknown-unknownwith nocfgchanges. -
Every crate has a documented public API under
#![deny(missing_docs)]. -
The boundary-enforcing
cargo treechecks run in CI. -
The same
terminal-coreserves four consumers unmodified: the GUI, the mux server, the CLI, and the test harness. - A written boundary defense: per crate, what it must not know and the capability lost if the boundary broke.
- A configuration design that is passed down, not reached up for.
- Optional and last: a C ABI with a working C example program.
Common Mistakes in This Section
| Mistake | Consequence |
|---|---|
| Designing the crate split before building anything | Beautiful names around code you do not understand |
| A global config that every layer reads | Everything depends on everything; the core stops being portable |
| Exposing internals in the public API "for now" | You cannot change them later without a breaking release |
| Building the FFI before the Rust API settles | The FFI freezes accidents |
| One consumer and calling it a boundary | Untested by construction |
pub on everything | The API surface is whatever you forgot to make private |
Platform cfg outside terminal-pty | The core is no longer portable and you will not notice until WASM |
| A callback-based FFI with Rust panics crossing the boundary | Undefined behavior. Panics must never unwind into C. |
Section Profile: What a Section 5 Graduate Can Do
| Capability | Evidence |
|---|---|
| Defend a crate boundary | For each, the concrete capability it protects |
| Recognize the pattern in other projects | Read alacritty_terminal, vte, termwiz and map them onto your crates |
| Design an embeddable engine | Four working consumers |
| Decide when to add an FFI | The ordering rule, and the reason for it |
| Explain what libghostty is for | Native UI per platform over one shared engine |
Next: Crate Boundaries.