chore: scaffold Cargo workspace for the Ghostty rewrite

Start of the full Rust rewrite of Ghostty 1.3.1 (reference source in
../ghostty). Sets up a workspace whose crates mirror Ghostty's Zig modules
(vt, unicode, datastruct, os, input, config, terminal, font, renderer,
termio, core, apprt-gtk) so porting work can proceed in parallel per crate.

Also adds:
- docs/rewrite/{ARCHITECTURE,PLAN,STATUS,AGENT_GUIDE}.md: crate map, core
  design decisions (safe Rust, slab-based PageList, logical capacity
  accounting), phased plan, progress tracker, and porting rules.
- Workspace clippy config relaxing pedantic lints that are pure noise for
  low-level terminal code (casts, long functions).
- Shared vt types needed by several work packages up front: kitty
  keyboard flags (kitty/key.rs) and the CSI separator bitset (SepList).

Test Plan:
- cargo test -p ghostty-vt (3 tests pass)
- cargo clippy -p ghostty-vt --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-11 21:41:11 +02:00
1 parent e04991c1f5
commit 987f2957ad
34 files changed
+759 -4

No files matched your search

Generated
+96
View File
@@ -2,6 +2,102 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "ghostty-apprt-gtk"
version = "0.1.0"
dependencies = [
"ghostty-config",
"ghostty-core",
"ghostty-input",
]
[[package]]
name = "ghostty-config"
version = "0.1.0"
dependencies = [
"ghostty-input",
"ghostty-os",
"ghostty-vt",
]
[[package]]
name = "ghostty-core"
version = "0.1.0"
dependencies = [
"ghostty-config",
"ghostty-datastruct",
"ghostty-font",
"ghostty-input",
"ghostty-os",
"ghostty-renderer",
"ghostty-terminal",
"ghostty-termio",
]
[[package]]
name = "ghostty-datastruct"
version = "0.1.0"
[[package]]
name = "ghostty-font"
version = "0.1.0"
dependencies = [
"ghostty-config",
"ghostty-terminal",
"ghostty-unicode",
]
[[package]]
name = "ghostty-input"
version = "0.1.0"
dependencies = [
"ghostty-vt",
]
[[package]]
name = "ghostty-os"
version = "0.1.0"
[[package]]
name = "ghostty-renderer"
version = "0.1.0"
dependencies = [
"ghostty-config",
"ghostty-font",
"ghostty-terminal",
]
[[package]] [[package]]
name = "ghostty-rs" name = "ghostty-rs"
version = "0.1.0" version = "0.1.0"
dependencies = [
"ghostty-apprt-gtk",
"ghostty-config",
"ghostty-core",
]
[[package]]
name = "ghostty-terminal"
version = "0.1.0"
dependencies = [
"ghostty-datastruct",
"ghostty-unicode",
"ghostty-vt",
]
[[package]]
name = "ghostty-termio"
version = "0.1.0"
dependencies = [
"ghostty-config",
"ghostty-os",
"ghostty-terminal",
]
[[package]]
name = "ghostty-unicode"
version = "0.1.0"
[[package]]
name = "ghostty-vt"
version = "0.1.0"
+62 -4
View File
@@ -2,16 +2,74 @@
name = "ghostty-rs" name = "ghostty-rs"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "A Rust rewrite of the Ghostty terminal emulator"
[[bin]]
name = "ghostty"
path = "src/main.rs"
[dependencies] [dependencies]
ghostty-core.workspace = true
ghostty-config.workspace = true
ghostty-apprt-gtk.workspace = true
[lints.clippy] [lints]
workspace = true
[workspace]
resolver = "3"
members = [".", "crates/*"]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"
[workspace.dependencies]
ghostty-vt = { path = "crates/ghostty-vt" }
ghostty-unicode = { path = "crates/ghostty-unicode" }
ghostty-terminal = { path = "crates/ghostty-terminal" }
ghostty-input = { path = "crates/ghostty-input" }
ghostty-config = { path = "crates/ghostty-config" }
ghostty-font = { path = "crates/ghostty-font" }
ghostty-renderer = { path = "crates/ghostty-renderer" }
ghostty-termio = { path = "crates/ghostty-termio" }
ghostty-os = { path = "crates/ghostty-os" }
ghostty-datastruct = { path = "crates/ghostty-datastruct" }
ghostty-core = { path = "crates/ghostty-core" }
ghostty-apprt-gtk = { path = "crates/ghostty-apprt-gtk" }
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 }
todo = "warn" todo = "warn"
unwrap_used = "warn" unwrap_used = "warn"
# Porting a low-level terminal emulator involves a lot of intentional
[lints.rust] # numeric casts and long state-machine functions; these pedantic lints are
unsafe_code = "forbid" # noise for this codebase.
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_sign_loss = "allow"
cast_precision_loss = "allow"
cast_lossless = "allow"
module_name_repetitions = "allow"
too_many_lines = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
similar_names = "allow"
struct_excessive_bools = "allow"
fn_params_excessive_bools = "allow"
match_same_arms = "allow"
unreadable_literal = "allow"
many_single_char_names = "allow"
doc_markdown = "allow"
must_use_candidate = "allow"
return_self_not_must_use = "allow"
used_underscore_binding = "allow"
large_stack_arrays = "allow"
inline_always = "allow"
[profile.release] [profile.release]
debug = true debug = true
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "ghostty-apprt-gtk"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-core.workspace = true
ghostty-config.workspace = true
ghostty-input.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-apprt-gtk: see docs/rewrite/ARCHITECTURE.md for the module map.
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "ghostty-config"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-input.workspace = true
ghostty-os.workspace = true
ghostty-vt.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-config: see docs/rewrite/ARCHITECTURE.md for the module map.
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "ghostty-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-config.workspace = true
ghostty-terminal.workspace = true
ghostty-input.workspace = true
ghostty-font.workspace = true
ghostty-renderer.workspace = true
ghostty-termio.workspace = true
ghostty-os.workspace = true
ghostty-datastruct.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-core: see docs/rewrite/ARCHITECTURE.md for the module map.
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ghostty-datastruct"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-datastruct: see docs/rewrite/ARCHITECTURE.md for the module map.
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "ghostty-font"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-config.workspace = true
ghostty-unicode.workspace = true
ghostty-terminal.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-font: see docs/rewrite/ARCHITECTURE.md for the module map.
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ghostty-input"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-vt.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-input: see docs/rewrite/ARCHITECTURE.md for the module map.
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ghostty-os"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-os: see docs/rewrite/ARCHITECTURE.md for the module map.
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "ghostty-renderer"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-config.workspace = true
ghostty-font.workspace = true
ghostty-terminal.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-renderer: see docs/rewrite/ARCHITECTURE.md for the module map.
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "ghostty-terminal"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-vt.workspace = true
ghostty-unicode.workspace = true
ghostty-datastruct.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-terminal: see docs/rewrite/ARCHITECTURE.md for the module map.
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "ghostty-termio"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ghostty-config.workspace = true
ghostty-terminal.workspace = true
ghostty-os.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-termio: see docs/rewrite/ARCHITECTURE.md for the module map.
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ghostty-unicode"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
//! ghostty-unicode: see docs/rewrite/ARCHITECTURE.md for the module map.
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ghostty-vt"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
[lints]
workspace = true
+173
View File
@@ -0,0 +1,173 @@
//! Kitty keyboard protocol support (ZIG/src/terminal/kitty/key.zig).
/// Stack for the key flags. This implements the push/pop behavior
/// of the CSI > u and CSI < u sequences. We implement the stack as
/// fixed size to avoid heap allocation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FlagStack {
pub flags: [Flags; FlagStack::LEN],
/// Index into `flags`; wraps modulo `LEN` (a `u3` in Zig).
pub idx: u8,
}
impl FlagStack {
pub const LEN: usize = 8;
/// Return the current stack value
pub fn current(&self) -> Flags {
self.flags[self.idx as usize]
}
/// Perform the "set" operation as described in the spec for
/// the CSI = u sequence.
pub fn set(&mut self, mode: SetMode, v: Flags) {
let cur = &mut self.flags[self.idx as usize];
*cur = match mode {
SetMode::Set => v,
SetMode::Or => Flags::from_int(cur.int() | v.int()),
SetMode::Not => Flags::from_int(cur.int() & !v.int()),
};
}
/// Push a new set of flags onto the stack. If the stack is full
/// then the oldest entry is evicted.
pub fn push(&mut self, flags: Flags) {
self.idx = (self.idx + 1) % Self::LEN as u8;
self.flags[self.idx as usize] = flags;
}
/// Pop `n` entries from the stack. This will just wrap around
/// if `n` is greater than the amount in the stack.
pub fn pop(&mut self, n: usize) {
// If n is more than our length then we just reset the stack.
// This also avoids a DoS vector where a malicious client
// could send a huge number of pop commands to waste cpu.
if n >= Self::LEN {
self.idx = 0;
self.flags = [Flags::DISABLED; Self::LEN];
return;
}
for _ in 0..n {
self.flags[self.idx as usize] = Flags::DISABLED;
self.idx = (self.idx + Self::LEN as u8 - 1) % Self::LEN as u8;
}
}
}
/// The possible flags for the Kitty keyboard protocol.
/// Bit order matches the Zig `packed struct(u5)` (disambiguate = bit 0).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct Flags {
pub disambiguate: bool,
pub report_events: bool,
pub report_alternates: bool,
pub report_all: bool,
pub report_associated: bool,
}
impl Flags {
/// Kitty keyboard protocol disabled (all flags off).
pub const DISABLED: Flags = Flags {
disambiguate: false,
report_events: false,
report_alternates: false,
report_all: false,
report_associated: false,
};
/// Sets all modes on.
pub const TRUE: Flags = Flags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
};
/// The flags as a 5-bit integer.
pub fn int(self) -> u8 {
u8::from(self.disambiguate)
| u8::from(self.report_events) << 1
| u8::from(self.report_alternates) << 2
| u8::from(self.report_all) << 3
| u8::from(self.report_associated) << 4
}
/// Build flags from the low 5 bits of `v`.
pub fn from_int(v: u8) -> Flags {
Flags {
disambiguate: v & 1 != 0,
report_events: v & 2 != 0,
report_alternates: v & 4 != 0,
report_all: v & 8 != 0,
report_associated: v & 16 != 0,
}
}
}
/// The possible modes for setting the Kitty keyboard protocol flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SetMode {
Set = 1,
Or = 2,
Not = 3,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_stack_overflow() {
let mut stack = FlagStack {
idx: (FlagStack::LEN - 1) as u8,
..FlagStack::default()
};
stack.push(Flags::DISABLED);
assert_eq!(stack.idx, 0);
stack.pop(1);
assert_eq!(stack.idx as usize, FlagStack::LEN - 1);
}
#[test]
fn flags_int() {
assert_eq!(Flags::DISABLED.int(), 0b0_0000);
assert_eq!(
Flags {
disambiguate: true,
..Flags::DISABLED
}
.int(),
0b0_0001
);
assert_eq!(
Flags {
report_events: true,
..Flags::DISABLED
}
.int(),
0b0_0010
);
assert_eq!(Flags::TRUE.int(), 0b1_1111);
assert_eq!(Flags::from_int(0b1_0100).int(), 0b1_0100);
}
#[test]
fn flag_stack_set_push_pop() {
let mut stack = FlagStack::default();
stack.set(SetMode::Set, Flags::from_int(1));
stack.set(SetMode::Or, Flags::from_int(4));
assert_eq!(stack.current().int(), 5);
stack.set(SetMode::Not, Flags::from_int(1));
assert_eq!(stack.current().int(), 4);
stack.push(Flags::from_int(2));
assert_eq!(stack.current().int(), 2);
stack.pop(1);
assert_eq!(stack.current().int(), 4);
stack.pop(100);
assert_eq!(stack, FlagStack::default());
}
}
+3
View File
@@ -0,0 +1,3 @@
//! Kitty protocol types (ZIG/src/terminal/kitty.zig).
pub mod key;
+5
View File
@@ -0,0 +1,5 @@
//! ghostty-vt: VT parsing layer of Ghostty (parser, OSC/CSI/DCS/APC, SGR,
//! colors, modes, stream). See docs/rewrite/ARCHITECTURE.md.
pub mod kitty;
pub mod parser;
+52
View File
@@ -0,0 +1,52 @@
//! VT-series parser for escape and control sequences
//! (ZIG/src/terminal/Parser.zig).
//!
//! NOTE(W1): this file is owned by work package W1. `MAX_PARAMS` and
//! `SepList` were pre-created by the orchestrator because `sgr` (W2)
//! depends on them; keep their API stable.
/// Maximum number of CSI parameters. This is a const in Ghostty so we
/// match it.
pub const MAX_PARAMS: usize = 24;
/// Bitset of length `MAX_PARAMS` marking which parameters were followed by
/// a colon (`:`) separator rather than a semicolon (`;`). Bit `i` set means
/// the separator *after* param `i` was a colon.
/// Equivalent to Zig's `std.StaticBitSet(MAX_PARAMS)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct SepList(u32);
impl SepList {
/// An empty set (all separators are semicolons).
pub const fn init_empty() -> Self {
Self(0)
}
/// Whether bit `i` is set (separator after param `i` is a colon).
pub fn is_set(&self, i: usize) -> bool {
debug_assert!(i < MAX_PARAMS);
self.0 & (1 << i) != 0
}
/// Set bit `i`.
pub fn set(&mut self, i: usize) {
debug_assert!(i < MAX_PARAMS);
self.0 |= 1 << i;
}
/// Clear bit `i`.
pub fn unset(&mut self, i: usize) {
debug_assert!(i < MAX_PARAMS);
self.0 &= !(1 << i);
}
/// Number of set bits.
pub fn count(&self) -> usize {
self.0.count_ones() as usize
}
/// Raw mask.
pub fn mask(&self) -> u32 {
self.0
}
}
+56
View File
@@ -0,0 +1,56 @@
# Guide for porting agents
You are porting part of Ghostty 1.3.1 (Zig) to Rust. Read
`docs/rewrite/ARCHITECTURE.md` first.
- Zig source (read-only reference): `../../../ghostty/src`
- Rust workspace: `../..`
## Rules
1. **Stay in your lane.** Only edit the crate/modules assigned to you. If you
need something from another crate that doesn't exist yet, write a minimal
local version, mark it `// TODO(integration): ...`, and mention it in your
report. Never rewrite other agents' modules. You MAY add dependencies to your
own crate's `Cargo.toml` (prefer well-known crates; use `cargo add -p <crate>`).
2. **Faithful port.** Preserve behaviour, edge cases, defaults and naming
(snake_case of the Zig names). Port doc comments (they are user-visible for
config/actions). Don't "improve" semantics. Idiomatic Rust structure is fine
(enums for tagged unions, traits for comptime interfaces, `Result` for error
unions) as long as behaviour matches.
3. **Port the tests.** Every Zig `test "name"` in files you port becomes a
`#[test] fn name_snake_case()` in a `#[cfg(test)] mod tests` of the
corresponding Rust module. Tests that are about Zig-only concerns
(allocator failure injection, comptime, C ABI, Metal/macOS) may be skipped —
list them in your notes file. Add extra tests when behaviour is subtle.
4. **No `unsafe`** unless your crate is explicitly allowed (renderer, termio,
apprt-gtk), and then only minimal + `// SAFETY:` comments.
5. **Quality gate before you finish** (must all pass for your crate):
- `cargo build -p <crate>`
- `cargo test -p <crate>`
- `cargo clippy -p <crate> --all-targets -- -D warnings`
- `cargo fmt -p <crate>` (rustfmt.toml in repo root; if nightly-only options
warn, ignore the warnings)
No `todo!()`/`unimplemented!()` left in code paths that are reachable,
unless documented in your notes as deferred.
6. **Do NOT run `git commit`** or any git command that changes state. The
orchestrator commits.
7. Other agents work concurrently in other crates of the same workspace. If the
*workspace* build breaks because of another crate, use `-p <crate>` and
ignore it. Cargo may block briefly on the build-directory lock; that's normal.
8. **Write notes** to `docs/rewrite/notes/<crate>-<topic>.md`: what was ported
(file → module), skipped tests with reasons, deviations, TODO(integration)
items, known gaps.
9. **Final report** (your last message): ≤ 25 lines — modules done, test counts
(ported / skipped), quality-gate status, open issues. The orchestrator reads
only this, so be precise and honest about anything incomplete.
## Conventions
- `pub mod` per Zig file; file names snake_case (`PageList.zig` → `page_list.rs`).
- Error types: `thiserror` is fine; `anyhow` only in binaries/CLI.
- Logging: the `log` crate (`log::warn!` etc.) where Zig uses `std.log`.
- Integer types: keep the Zig widths (u16 for cell counts `CellCountInt`, etc.).
- Zig `packed struct` bitfields → plain structs unless memory layout matters
(documented in ARCHITECTURE for `Cell`/`Row`).
- Prefer `std` collections; `hashbrown`/`smallvec`/`bitflags` OK.
+76
View File
@@ -0,0 +1,76 @@
# Ghostty-rs Architecture
Rust rewrite of Ghostty 1.3.1. Reference source: `../ghostty/` (Zig), referred
to below as `ZIG/` (= `../../../ghostty`).
Target platform: **Linux (GTK4 + libadwaita, OpenGL)**. macOS/Metal/CoreText,
WASM, Windows and the embedded C API (`libghostty`) are out of scope.
## Crate map (workspace `crates/`)
| Crate | Ports (ZIG/src/...) | Depends on |
|---|---|---|
| `ghostty-vt` | terminal/Parser, parse_table, UTF8Decoder, osc.zig + osc/*, csi, dcs, apc, sgr, color, x11_color, charsets, modes, ansi, device_status, mouse_shape, kitty/key, kitty/color, kitty/graphics_command, stream.zig | – |
| `ghostty-unicode` | unicode/* (width, grapheme break, props) | – |
| `ghostty-datastruct` | datastruct/* (split_tree, lru, circ_buf, blocking_queue, cache_table, ...) | – |
| `ghostty-os` | os/* (xdg, homedir, shell, locale, passwd, env, open, desktop, resourcesdir, hostname, TempDir, uri, ...) | – |
| `ghostty-input` | input/* (key, key_mods, keycodes, function_keys, key_encode, Binding, command, mouse, paste, kitty, Link) | vt |
| `ghostty-config` | config/*, cli/args.zig, cli/CommaSplitter.zig, cli/diagnostics.zig | input, os, vt |
| `ghostty-terminal` | terminal/ page, PageList, Screen, ScreenSet, Terminal, style, hyperlink, Tabstops, size, point, Selection, formatter, render, highlight, search/*, kitty/graphics_{image,storage,exec,unicode,render}, tmux/*, stream_readonly, StringMap, ref_counted_set, bitmap_allocator semantics | vt, unicode, datastruct |
| `ghostty-font` | font/* (Metrics, sprite/*, Atlas, discovery via fontconfig, face via freetype, shaper via harfbuzz, Collection, CodepointResolver, SharedGrid(Set), opentype/*, nerd_font_attributes) | config, unicode, terminal |
| `ghostty-renderer` | renderer/ generic, OpenGL (+ opengl/*), cell, cursor, image, link, size, State, Thread, Overlay, shadertoy, row, shaders/glsl | config, font, terminal |
| `ghostty-termio` | termio/*, pty.zig, Command.zig, shell-integration resources | config, terminal, os |
| `ghostty-core` | App.zig, Surface.zig, surface_mouse.zig, apprt/action.zig, apprt/structs.zig, apprt/surface.zig, global.zig, cli/* actions (+list-fonts, +show-config, ...) | everything below |
| `ghostty-apprt-gtk` | apprt/gtk/* | core, config, input |
| root `ghostty-rs` (bin `ghostty`) | main_ghostty.zig, build_config.zig | core, config, apprt-gtk |
## Key design decisions
1. **Safe Rust by default.** Workspace lint `unsafe_code = "forbid"`. Crates
that must do FFI/syscalls (renderer GL loader, termio pty/fork, apprt-gtk)
may override with `unsafe_code = "deny"` and `#[allow(unsafe_code)]` on
minimal, `// SAFETY:`-commented items. Prefer safe wrapper crates (rustix,
glow, gtk4-rs) over raw FFI.
2. **Pages without raw memory.** Ghostty's `page.zig` packs a page into one
mmap'd buffer with offsets. In Rust a `Page` owns `Vec<Row>` + `Vec<Cell>`
(row-major, `cols` per row) plus per-page `RefCountedSet<Style>`, grapheme
map `HashMap<cell_index, Vec<u32>>`, hyperlink set/map, string storage.
**Capacity accounting is preserved logically** (styles, grapheme bytes,
hyperlink bytes, string bytes, rows/cols), including Ghostty's byte-size
layout formula (`Page::layout(cap).total_size`), because PageList pruning
(`scrollback-limit` is in bytes), page splitting and capacity-adjust
behaviour — and the tests for them — depend on it.
3. **PageList as an index-based linked list.** Nodes live in a slab
(`Vec<Option<Node>>` + free list); `NodeId` is an index (+ generation for
debug checks). `Pin { node: NodeId, y, x, garbage }` is `Copy`. Tracked pins
live in a PageList-owned slab; holders keep a `TrackedPin` handle
(index) and read/write through the PageList. All operations that move
rows update tracked pins exactly like the Zig code.
4. **Cell is a compact `Copy` value** (target 8 bytes, bit-packed u64 with
accessor methods mirroring Zig field names), so memory accounting and
copying behaviour stay close to Ghostty.
5. **Parser → Stream → Handler.** `ghostty-vt::Parser` yields actions;
`ghostty-vt::stream::Stream<H: Handler>` decodes CSI/ESC/OSC/DCS/APC into
high-level actions and calls `Handler`. Terminal state changes live in
`ghostty-terminal` (`stream_readonly` handler applies to `Terminal`), and
`ghostty-termio::StreamHandler` handles side-effecting actions.
6. **Rendering:** OpenGL 4.3 via `glow`, inside a `gtk::GLArea`. Shaders are
copied verbatim from `ZIG/src/renderer/shaders/glsl`. Renderer logic
follows `renderer/generic.zig` (cell contents rebuilt from
`terminal::render::RenderState`).
7. **Fonts:** fontconfig (discovery), freetype (rasterization), harfbuzz
(shaping). Sprite font (box drawing etc.) is pure Rust, ported 1:1.
8. **Threads:** like Ghostty — per surface: IO thread (termio), renderer
thread, plus a pty read thread. Mailboxes are `crossbeam-channel` /
`std::sync::mpsc`; the GTK main thread is woken via `glib` channels.
The `Terminal` is shared behind `Arc<Mutex<..>>` (Ghostty's renderer mutex).
9. **Config:** same file format, same keys, same defaults, same CLI flags,
same docs text (used by `+show-config --docs` and `--help`).
10. **Tests:** every Zig `test "..."` block in a ported file is ported to a
Rust `#[test]` with the same name (snake_cased) in the same module.
`scripts/test-progress.sh` tracks counts.
## Out of scope / deferred
- macOS (Swift app, Metal, CoreText), Windows, WASM, libghostty C API.
- The ImGui inspector is deferred until everything else works (Phase 7).
+43
View File
@@ -0,0 +1,43 @@
# Rewrite plan
Orchestrator: Opus (plans, designs core abstractions, reviews, integrates,
commits). Bulk porting: Sonnet subagents. Hard algorithmic cores: Codex
(gpt-6-astra) or Opus. Progress lives in `STATUS.md`.
## Phases
### Phase 0 — scaffolding (done)
Workspace, crate skeletons, docs, lints.
### Phase 1 — leaf crates (parallel)
- W1 `ghostty-vt` part A: Parser, parse_table, UTF8Decoder, osc + osc/parsers/*, osc/encoding, color, x11_color, kitty/color.
- W2 `ghostty-vt` part B: csi (incl. `SepList`), sgr, modes, charsets, ansi, device_status, mouse_shape, kitty/key, kitty/graphics_command, apc, dcs.
- W3 `ghostty-unicode` + `ghostty-datastruct`.
- W4 `ghostty-os`.
- W5 `ghostty-input`.
- W6 `ghostty-config` (+ cli/args, CommaSplitter, diagnostics).
- W7 `ghostty-font`: Metrics + sprite/*.
### Phase 2 — terminal core
- `ghostty-vt::stream` (stream.zig) once W1/W2 land.
- `ghostty-terminal`: style, hyperlink, size, point, ref_counted_set, page, PageList (Codex/Opus: hardest part), then Screen, Tabstops, Terminal, ScreenSet, Selection, stream_readonly, formatter, render, highlight, search/*, kitty graphics storage/image/exec/unicode, tmux.
### Phase 3 — font engine
discovery (fontconfig), freetype face, harfbuzz shaper (+run, feature, Cache), opentype tables, Collection, CodepointResolver, CodepointMap, DeferredFace, SharedGrid, SharedGridSet, Atlas, embedded fonts, nerd_font_attributes.
### Phase 4 — termio
pty, Command (fork/exec), Exec backend, Termio, Thread, mailbox, stream_handler, shell_integration (+ copy `ZIG/src/shell-integration`).
### Phase 5 — renderer
OpenGL backend (glow), generic renderer logic, cells, cursor, image, link, size, State, Thread, Overlay, shadertoy custom shaders.
### Phase 6 — core + GTK app
ghostty-core: App, Surface, surface_mouse, apprt action/structs, CLI actions. ghostty-apprt-gtk: application, windows, tabs, splits, surface widget (GLArea, input, IME, clipboard), config reload, dialogs, notifications, quick terminal, IPC (+new-window), CSS/UI.
### Phase 7 — integration and polish
End-to-end run, vttest-ish smoke tests, perf, inspector (maybe), docs, CHANGELOG.
## Review protocol
After each work package: orchestrator reads the agent report + notes, spot-checks
code (esp. tricky logic), runs quality gate on the workspace, commits. Larger
cores get an `/astrareview` or Codex review pass.
+17
View File
@@ -0,0 +1,17 @@
# Status
Legend: ⬜ todo · 🟨 in progress · ✅ done (gate passed, committed) · ⚠️ done with gaps
| WP | Scope | Owner | State | Notes |
|---|---|---|---|---|
| P0 | scaffolding | opus | ✅ | |
| W1 | vt: parser/osc/color | sonnet | ⬜ | |
| W2 | vt: csi/sgr/modes/kitty/apc/dcs | sonnet | ⬜ | |
| W3 | unicode + datastruct | sonnet | ⬜ | |
| W4 | os | sonnet | ⬜ | |
| W5 | input | sonnet | ⬜ | |
| W6 | config | sonnet | ⬜ | |
| W7 | font: metrics + sprite | sonnet | ⬜ | |
## Log
- 2026-10-11: Phase 0 complete; Phase 1 launched.
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Compare number of Zig tests vs Rust tests per area.
set -euo pipefail
ZIG=$(cd "$(dirname "$0")/.." && pwd)/../ghostty/src
RS=$(cd "$(dirname "$0")/.." && pwd)/crates
printf "%-12s %6s\n" area zig_tests
for d in terminal font renderer input config termio cli os datastruct unicode apprt; do
printf "%-12s %6s\n" "$d" "$(grep -rh '^test "' "$ZIG/$d" | wc -l)"
done
echo
printf "%-22s %6s\n" crate rust_tests
for c in "$RS"/*/; do
printf "%-22s %6s\n" "$(basename "$c")" "$(grep -rhE '^\s*#\[(test|gtk::test)\]' "$c" 2>/dev/null | wc -l)"
done