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:
1 parent
e04991c1f5
commit
987f2957ad
34 files changed
+759
-4
No files matched your search
Generated
+96
@@ -2,6 +2,102 @@
|
||||
# It is not intended for manual editing.
|
||||
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]]
|
||||
name = "ghostty-rs"
|
||||
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
@@ -2,16 +2,74 @@
|
||||
name = "ghostty-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "A Rust rewrite of the Ghostty terminal emulator"
|
||||
|
||||
[[bin]]
|
||||
name = "ghostty"
|
||||
path = "src/main.rs"
|
||||
|
||||
[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 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
# Porting a low-level terminal emulator involves a lot of intentional
|
||||
# numeric casts and long state-machine functions; these pedantic lints are
|
||||
# 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]
|
||||
debug = true
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-apprt-gtk: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-config: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-core: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ghostty-datastruct"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-datastruct: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-font: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-input: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ghostty-os"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-os: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-renderer: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-terminal: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-termio: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ghostty-unicode"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1 @@
|
||||
//! ghostty-unicode: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ghostty-vt"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Kitty protocol types (ZIG/src/terminal/kitty.zig).
|
||||
|
||||
pub mod key;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Executable
+14
@@ -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
|
||||
Reference in new issue
Block a user