fix(config): match Zig's numeric grammar, NUL handling and diagnostics

Resolves the 6 differential gap groups in docs/rewrite/notes/review-
rv9-codex.md (details in final-Z11.md):
- ghostty_os::zig_fmt ports std.fmt.parseInt/parseUnsigned/parseFloat
  (underscores, 0x/0o/0b prefixes, hex floats, inf/nan) with the Zig std
  tests and replaces every int/float parse in config, keybind
  parameters, ssh-cache and desktop detection.
- Interior NUL no longer panics in font names and ends C strings like
  Zig's backing bytes (fontconfig, C config getters, title actions,
  clipboard callback, relative paths, config-file, shaders, background
  image, GTK CSS).
- GTK new-window arguments are trimmed of ASCII whitespace only.
- Command tokenization uses a port of std.process.ArgIteratorGeneral
  (incl. CR/LF separators) with its std tests.
- UTF-8 validation happens where Zig's field parsers do it, so error
  kinds match (metric modifiers, quick-terminal size, link, ...).

A scripted differential run of 202 fields x ~220 hostile/valid values
against /usr/bin/ghostty (+validate-config and +show-config: exit code,
stdout, stderr) now has zero mismatches besides build-specific defaults
(theme resources path, auto-update-channel, gtk-opengl-debug); it also
surfaced and fixed background-blur failure state, NaN formatting and
NaN/-0 finalize clamps. Regression tests: ghostty-config/tests/
zig_parity.rs.

Test Plan:
- cargo test --workspace --no-fail-fast (3456 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 12:35:37 +02:00
1 parent 7480f99d91
commit 79a7fd4f76
26 files changed
+1835 -228

No files matched your search

@@ -1138,12 +1138,13 @@ impl Application {
}
if let Some(v) = s.strip_prefix("--working-directory=") {
// D-Bus strings are plain UTF-8, including literal U+FDD0.
working_directory =
Some(ghostty_os::osbytes::EncodedOs::from_plain_text(v.trim()));
working_directory = Some(ghostty_os::osbytes::EncodedOs::from_plain_text(
trim_ascii_whitespace(v),
));
continue;
}
if let Some(v) = s.strip_prefix("--title=") {
title = Some(v.trim().to_owned());
title = Some(trim_ascii_whitespace(v).to_owned());
}
}
}
@@ -1869,3 +1870,30 @@ impl Drop for ReadyGuard {
fn scopeguard_ready() -> ReadyGuard {
ReadyGuard
}
/// `std.mem.trim(u8, v, &std.ascii.whitespace)`: only ASCII whitespace
/// (space, `\t`, `\n`, VT, FF, `\r`) is trimmed, never Unicode whitespace
/// such as U+00A0.
fn trim_ascii_whitespace(v: &str) -> &str {
v.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c'))
}
#[cfg(test)]
mod trim_tests {
use super::trim_ascii_whitespace;
#[test]
fn new_window_arguments_trim_only_ascii_whitespace() {
assert_eq!(
trim_ascii_whitespace(" \t\n\r\x0b\x0c/tmp/a b \n"),
"/tmp/a b"
);
assert_eq!(trim_ascii_whitespace("/tmp/name\u{a0}"), "/tmp/name\u{a0}");
assert_eq!(
trim_ascii_whitespace("\u{a0}title\u{2003} "),
"\u{a0}title\u{2003}"
);
assert_eq!(trim_ascii_whitespace("\x0btitle\x0b"), "title");
assert_eq!(trim_ascii_whitespace(" \t "), "");
}
}
+2 -2
View File
@@ -110,8 +110,8 @@ impl CssProviders {
for p in &config.gtk_custom_css.value {
let (path, optional) = match p {
Path::Optional(path) => (path.to_path(), true),
Path::Required(path) => (path.to_path(), false),
Path::Optional(path) => (path.to_syscall_path(), true),
Path::Required(path) => (path.to_syscall_path(), false),
};
let path = match path {
Ok(path) => path,