Files
ghostty-rs/crates/ghostty-vt/src/kitty/key.rs
T
ddidderr 987f2957ad 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
2026-10-11 21:41:11 +02:00

174 lines
5.1 KiB
Rust

//! 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());
}
}