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 70c250cf2d
commit 6f9b3129e1
100 files changed
+2310 -1054

No files matched your search

+10 -13
View File
@@ -158,7 +158,7 @@ impl Action {
///
/// If the action requests its help text (`--help`/`-h`), the help text
/// is printed to stdout and 0 is returned.
pub fn run(self, args: &[String]) -> Result<u8, Error> {
pub fn run(self, args: &[impl AsRef<[u8]>]) -> Result<u8, Error> {
match self.run_main(args) {
Err(Error::ActionHelpRequested) => {
// Future note: for now we just output the help text directly
@@ -180,7 +180,7 @@ impl Action {
}
}
fn run_main(self, args: &[String]) -> Result<u8, Error> {
fn run_main(self, args: &[impl AsRef<[u8]>]) -> Result<u8, Error> {
match self {
Action::Version => crate::version::run(args),
Action::Help => crate::help::run(args),
@@ -239,7 +239,7 @@ impl std::fmt::Display for Action {
}
/// Detect the CLI action from `args` (full argv, `argv[0]` included).
pub fn detect(args: &[String]) -> Result<Option<Action>, DetectError> {
pub fn detect(args: &[impl AsRef<[u8]>]) -> Result<Option<Action>, DetectError> {
detect_iter::<Action, _, _>(args.iter())
}
@@ -248,21 +248,21 @@ pub fn detect(args: &[String]) -> Result<Option<Action>, DetectError> {
/// reversible escape encoding of `ghostty_os::osbytes`, which the config
/// layer (`cli_args`) decodes back to the original bytes.
#[must_use]
pub fn os_args_to_strings(args: &[std::ffi::OsString]) -> Vec<String> {
pub fn os_args_to_bytes(args: &[std::ffi::OsString]) -> Vec<Vec<u8>> {
args.iter()
.map(|a| ghostty_os::osbytes::os_to_config_string(a))
.map(|a| ghostty_os::osbytes::os_bytes(a))
.collect()
}
/// [`detect`] over raw OS arguments.
pub fn detect_os(args: &[std::ffi::OsString]) -> Result<Option<Action>, DetectError> {
detect(&os_args_to_strings(args))
detect(&os_args_to_bytes(args))
}
/// [`run_from_args`] over raw OS arguments (byte-preserving).
#[must_use]
pub fn run_from_os_args(args: &[std::ffi::OsString]) -> Option<u8> {
run_from_args(&os_args_to_strings(args))
run_from_args(&os_args_to_bytes(args))
}
/// The CLI entrypoint logic of `main_ghostty.zig` for actions: detect the
@@ -272,7 +272,7 @@ pub fn run_from_os_args(args: &[std::ffi::OsString]) -> Option<u8> {
/// Returns `None` if no action was requested (the caller should launch the
/// terminal), or the exit code the process should terminate with.
#[must_use]
pub fn run_from_args(args: &[String]) -> Option<u8> {
pub fn run_from_args(args: &[impl AsRef<[u8]>]) -> Option<u8> {
let action = match detect(args) {
Ok(a) => a?,
Err(err) => {
@@ -370,11 +370,8 @@ mod tests {
std::ffi::OsString::from("+version"),
];
assert_eq!(Some(Action::Version), detect_os(&args).unwrap());
let strings = os_args_to_strings(&args);
assert_eq!(
ghostty_os::osbytes::config_str_to_bytes(&strings[1]),
b"--title=\xff"
);
let strings = os_args_to_bytes(&args);
assert_eq!(strings[1], b"--title=\xff");
}
#[test]