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 8dfec20bca
commit 864ef82acb
34 files changed
+759 -4

No files matched your search

+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
}
}