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?

ReasonConsequence
Native UI per platformA macOS app that feels like a macOS app, without a cross-platform toolkit's compromises
One implementation of the hard partsThe VT parser and screen model are written once and shared
TestabilityThe core is testable with no window, no GPU, no display server
EmbeddabilityThe same engine can power an IDE panel, a headless simulator, or a test harness
Language independenceA 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.

#ConcernYour crate
1Terminal protocol parsingterminal-protocol
2Terminal state and screen buffersterminal-core
3Input encodingterminal-input
4PTY and process integrationterminal-pty
5Rendering-independent terminal dataterminal-render-model
6Platform-specific integrationterminal-gui (windowing), terminal-pty (OS)
7Application UIterminal-gui
8Configuration(you probably do not have this yet — see below)
9Multiplexer logicterminal-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

ChapterCovers
Crate BoundariesEach crate's responsibility, forbidden knowledge, API, state, deps, tests, portability
Embedding ScenariosSix different consumers of one core, and what each proves
FFI and BindingsC ABI, Swift, C++, WebAssembly — after the Rust API is stable

The Lab

LabMilestoneBuild
Lab 20M13Stabilize 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-protocol and terminal-core compile for wasm32-unknown-unknown with no cfg changes.
  • Every crate has a documented public API under #![deny(missing_docs)].
  • The boundary-enforcing cargo tree checks run in CI.
  • The same terminal-core serves 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

MistakeConsequence
Designing the crate split before building anythingBeautiful names around code you do not understand
A global config that every layer readsEverything 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 settlesThe FFI freezes accidents
One consumer and calling it a boundaryUntested by construction
pub on everythingThe API surface is whatever you forgot to make private
Platform cfg outside terminal-ptyThe core is no longer portable and you will not notice until WASM
A callback-based FFI with Rust panics crossing the boundaryUndefined behavior. Panics must never unwind into C.

Section Profile: What a Section 5 Graduate Can Do

CapabilityEvidence
Defend a crate boundaryFor each, the concrete capability it protects
Recognize the pattern in other projectsRead alacritty_terminal, vte, termwiz and map them onto your crates
Design an embeddable engineFour working consumers
Decide when to add an FFIThe ordering rule, and the reason for it
Explain what libghostty is forNative UI per platform over one shared engine

Next: Crate Boundaries.