refactor(config): parse config values from raw bytes like Zig

Zig parses config values (cli/args.zig, Config.zig field parsers,
config/string.zig) from raw byte slices, and each field type decides
whether it requires UTF-8 (InvalidValue otherwise) or stores bytes. The
port escape-encoded raw bytes into Strings at the line reader and let
fields parse the escaped text, so fields could observe encoding
artifacts; successive audits kept finding such leaks (RV5-RV8).

Field parsers now receive the original bytes: text fields validate UTF-8
where Zig does, byte/path fields store bytes (EncodedOs/OsString). This
resolves all 9 RV8 findings (docs/rewrite/notes/review-rv8-codex.md),
each with a regression test: Windows path conversion fails with
BadPathName instead of substituting U+FFFD; Windows process launch uses
strict UTF-8 like Zig's Command; the WTF-8 decoder accepts paired-
surrogate encodings like Zig; literal U+FDD0 Unicode key triggers;
selection-word-chars and clipboard-codepoint-map reject invalid UTF-8;
font style / codepoint-map family names use original bytes; C title
actions forward bytes; D-Bus new-window/Flatpak conversions match Zig.
Design, sweep and validation: docs/rewrite/notes/final-bytes-refactor.md.

Also moves the bundled iTerm2-Color-Schemes license out of
resources/themes/ so +list-themes no longer lists it as a theme
(output is now byte-identical to Ghostty 1.3.1).

Done by Codex (gpt-6-astra), reviewed and verified by the orchestrator.

Test Plan:
- cargo test --workspace --no-fail-fast (3428 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
- +show-config --default --docs, +list-keybinds, +list-actions --docs,
  +list-themes --plain compared with /usr/bin/ghostty
This commit is contained in:
ddidderr committed 2026-10-12 11:57:01 +02:00
1 parent 37a1646611
commit 672e8a4ae1
100 files changed
+2310 -1054

No files matched your search

+8 -4
View File
@@ -92,7 +92,7 @@ impl RtApp for FakeRt {
let ret = match &action {
Action::ShowChildExited(_) => self.show_child_exited.get(),
Action::SetTitle(t) => {
*self.title.borrow_mut() = Some(t.title.clone());
*self.title.borrow_mut() = Some(t.title.to_text_lossy());
true
}
_ => true,
@@ -109,8 +109,8 @@ impl RtSurface for FakeRt {
fn cgroup(&self) -> Option<String> {
None
}
fn get_title(&self) -> Option<String> {
self.title.borrow().clone()
fn get_title(&self) -> Option<ghostty_os::osbytes::EncodedOs> {
self.title.borrow().clone().map(Into::into)
}
fn get_content_scale(&self) -> ContentScale {
self.scale.get()
@@ -262,7 +262,11 @@ impl Harness {
config.load_iter(&mut it).expect("config args");
if let Some(script) = &opts.script {
let script = script.replace("{out}", out.to_str().unwrap());
config.command = Some(Command::Direct(vec!["sh".into(), "-c".into(), script]));
config.command = Some(Command::Direct(vec![
"sh".into(),
"-c".into(),
script.into(),
]));
}
config.finalize().expect("finalize");
+2 -4
View File
@@ -369,10 +369,7 @@ fn text_csi_esc_and_ignore_bindings() {
#[test]
fn config_loaded_text_csi_esc_write_decoded_bytes() {
use ghostty_os::osbytes::EncodedOs;
let h = harness!(Harness::recorder("", &[]));
let enc = |raw: &[u8]| EncodedOs::from_bytes(raw).into_encoded();
// Byte FF and a literal U+FDD0 must reach the pty unchanged, not as
// the reversible escape encoding.
for (kind, raw, want) in [
@@ -386,7 +383,8 @@ fn config_loaded_text_csi_esc_write_decoded_bytes() {
("esc", "\u{fdd0}".as_bytes(), b"\x1b\xef\xb7\x90".to_vec()),
] {
h.mark();
let a = ghostty_input::binding::Action::parse(&format!("{kind}:{}", enc(raw))).unwrap();
let a = ghostty_input::binding::Action::parse_bytes(&[kind.as_bytes(), b":", raw].concat())
.unwrap();
assert!(h.surface.perform_binding_action(&a).unwrap());
h.expect_input(&want);
}