fix: use Zig's numeric parsing in protocol and escape-sequence parsers

Rust's str::parse accepts and rejects different inputs than Zig's
std.fmt.parseInt/parseFloat (underscores, base prefixes, signs). The
config layer already used the Zig ports; now the protocol parsers do
too (call site -> Zig reference -> status in final-Z12.md):
- zig_fmt moves into the leaf crate ghostty-zigfmt (ghostty-vt cannot
  depend on ghostty-os); ghostty-os re-exports it unchanged.
- ghostty-vt: kitty graphics keys, kitty text sizing, OSC 4/5/10-19
  color indices, hex color components and intensities, OSC 9 sleep and
  progress, OSC 133 exit codes, OSC 3008 fields, kitty color palette.
- ghostty-terminal: tmux layout, format variables and pane tabs.
- ghostty-os: URI ports and %XX decoding; ghostty-bench CLI arguments.
Expected values were cross-checked against the real Zig toolchain.

Test Plan:
- cargo test --workspace --no-fail-fast
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 12:45:55 +02:00
1 parent c8a85b8806
commit a1ed0cd776
22 files changed
+290 -186

No files matched your search

@@ -67,28 +67,8 @@ enum UpdateError {
/// Equivalent of Zig `std.fmt.parseInt(u4, value, 10)`.
fn parse_u4(value: &[u8]) -> Option<u8> {
let (neg, digits) = match value.first() {
Some(b'+') => (false, &value[1..]),
Some(b'-') => (true, &value[1..]),
_ => (false, value),
};
if digits.is_empty() {
return None;
}
let mut acc: u32 = 0;
for &c in digits {
if !c.is_ascii_digit() {
return None;
}
acc = acc * 10 + (c - b'0') as u32;
if acc > 15 {
return None;
}
}
if neg && acc != 0 {
return None;
}
Some(acc as u8)
let v = ghostty_zigfmt::parse_int::<i32>(value, 10).ok()?;
u8::try_from(v).ok().filter(|v| *v <= 15)
}
impl OSC<'_> {
@@ -213,6 +193,20 @@ pub fn parse<S: ByteStorage>(
mod tests {
use super::*;
#[test]
fn zig_parse_u4_differences() {
assert_eq!(Some(10), parse_u4(b"1_0"));
assert_eq!(Some(5), parse_u4(b"+5"));
assert_eq!(Some(0), parse_u4(b"-0"));
assert_eq!(Some(15), parse_u4(b"15"));
assert_eq!(None, parse_u4(b"16"));
assert_eq!(None, parse_u4(b"5_0_0"));
assert_eq!(None, parse_u4(b"-1"));
assert_eq!(None, parse_u4(b"0x1"));
assert_eq!(None, parse_u4(b"1_"));
assert_eq!(None, parse_u4(b"99999999999"));
}
fn run(input: &[u8]) -> Option<Command<'static>> {
let mut p = Parser::init(false);
for &ch in input {