Files
ghostty-rs/docs/rewrite/notes/final-Z9.md
T
ddidderr e6564cb838 fix: systematic byte fidelity for config strings and RV7 gaps
Resolves the 9 gap groups in docs/rewrite/notes/review-rv7-codex.md and
audits every string-typed config field against its consumers
(field -> consumers -> decode status table in final-Z9.md):
- Windows EnvMap keys use a port of Zig's nls.zig uppercase table
  (upcaseW); Windows locale init follows Zig's chain (setlocale +
  _putenv_s); malformed WTF-8 now fails process launch conversions like
  Zig instead of substituting U+FFFD; kitty file paths convert WTF-8.
- bash --rcfile/--init-file, configured TERM and inherited GTK LANG
  keep their original bytes.
- C API clipboard completion and ghostty_surface_binding_action take
  bytes; complete_clipboard_request is byte-typed through the core.
- text:/csi:/esc: bindings, enquiry-response, input = path:, embedded
  initial_input, search needles, titles, link regexes, clipboard
  codepoint map, command palette text and GTK font/namespace values
  decode the escape encoding before use.

Remaining by design: D-Bus a{ss} values and +new-window arguments are
UTF-8 by D-Bus type. C API key/preedit text follows separately.

Test Plan:
- cargo test --workspace
- just check-macos (both), just check-windows, just check-wasm
2026-10-12 10:50:20 +02:00

9.5 KiB

Z9: RV7 gaps and the string-field decode audit

Builds on final-Z7.md / final-Z8.md (EncodedOs, the reversible U+FDD0 escape in ghostty_os::osbytes). Contract used below:

  • encoded = a config/CLI String in the reversible encoding. It must be decoded (EncodedOs::{to_os,to_path,to_bytes,to_wide}, config_str_to_bytes, or for UTF-8-only UI text config_str_to_text) before it reaches the OS, the pty, the clipboard, or C.
  • plain = real text; it may only become an encoded value through from_plain_text / valid_str_to_config_string (escapes a literal U+FDD0).

New helpers: osbytes::{InvalidWtf8, try_wtf8_to_wide, config_str_to_text}, EncodedOs::{try_to_wide, try_to_wide_nul, try_to_os}, ghostty_config::string::parse_bytes, ghostty_os::windows_nls::upcase_w.

1. The nine RV7 gaps

# Fix Test
1 Windows env key casing windows_nls.rs is a port of std/os/windows/nls.zig (the 2544-entry l_intl.nls table and upcaseW); EnvMap keys use it per UTF-16 unit instead of char::to_uppercase windows_nls::tests::*, env::windows_key_tests::keys_use_the_windows_table_not_unicode_mappings (U+1F80 == U+1F88, Georgian distinct, no ß == SS)
2 Windows locale init locale.rs posix module now also builds on Windows: setlocale with Zig's literal LC_ALL = 6 (the UCRT rejects it exactly like Zig, so the chain continues) and _putenv_s for the LANG updates (unsafe with SAFETY comments). ensure_locale is only a no-op on wasm the mock-Sys chain tests run everywhere; Windows glue compiled by check-windows
3 bash --rcfile GHOSTTY_BASH_RCFILE = EncodedOs::from_encoded(arg).to_os() bash_rcfile_keeps_original_bytes (byte FF, literal U+FDD0)
4 configured TERM termio Config.term is now EncodedOs; Unix exports to_os(), Windows try_to_os()? configured_term_is_exported_decoded
5 GTK inherited LANG saved_language is Option<OsString> read with var_os; the language config override is decoded; set_env takes AsRef<OsStr>; env restore factored into restore_language inherited_lang_keeps_non_utf8_bytes
6 Windows Kitty paths os_path_from_bytes: Unix bytes, Windows WTF-8 via EncodedOs::try_to_os (lone surrogates ok, malformed -> InvalidData, no empty-path fallback) payload_path_bytes_are_kept
7 malformed WTF-8 try_wtf8_to_wide (strict; lossy wtf8_to_wide kept for non-launch use). Command::start (path, cwd, argv) uses try_to_wide* -> StartError::InvalidWtf8; the Windows exec env block/overrides/GHOSTTY_RESOURCES_DIR/TERM/TERMINFO/PWD use try_to_os -> Error::InvalidWtf8 strict_wtf8_rejects_malformed_input, malformed_wtf8_env_overrides_fail (cfg(windows))
8 embedded C API ghostty_surface_complete_clipboard_request passes bytes (CoreSurface::complete_clipboard_request / Surface::complete_clipboard_request now take &[u8]; the confirm callback gets the caller's text pointer unchanged); ghostty_surface_binding_action encodes the bytes with bytes_to_config_string before Action::parse clipboard_completion_and_binding_action_keep_raw_bytes (child receives 61 ff / 62 ff)
9 text/csi/esc bindings Text uses new string::parse_bytes; Csi/Esc write config_str_to_bytes(data) (128-byte limit applies to bytes). string::parse now escapes a literal U+FDD0 that an escape sequence produces config_loaded_text_csi_esc_write_decoded_bytes, string::tests::parse_bytes_decodes_the_reversible_encoding

2. Systematic audit: config fields -> consumers -> decode status

"OK" = decoded at the boundary (or is the typed EncodedOs/bytes); "plain" = guaranteed text with no OS meaning; "FIXED" = changed in Z9.

Field / type Consumers that reach OS / pty / clipboard / C Status
language (String) GTK LANG override FIXED (decoded; was raw encoded)
font-family* (RepeatableString) Descriptor.family -> fontconfig / CoreText FIXED (config_str_to_text in shared_grid_set::Key::new; font names are UTF-8 for every backend)
font-feature HarfBuzz/CoreText feature tags plain (ASCII tags parsed from text)
font-variation*, font-codepoint-map, adjust-*, font-style* descriptors/metrics plain (tags, numbers, names)
clipboard-codepoint-map replacement clipboard text from the formatter FIXED (decode_codepoint_map; the formatter emits text, so non-UTF-8 replacement bytes can only become U+FFFD, a literal U+FDD0 is restored)
title SetTitle action (all apprts, C set_title) FIXED (config_str_to_text in core + GTK window)
class GTK application id (id_is_valid) plain (must be an ASCII-ish app id; encoded junk is rejected as invalid)
window-title-font-family GTK CSS FIXED (decoded)
gtk-quick-terminal-namespace layer-shell namespace (&str API) FIXED (decoded text)
macos-custom-icon only via C ghostty_config_get OK (config_capi::cstring_value decodes)
term child env TERM (Unix, Windows) FIXED (EncodedOs through exec::Config)
enquiry-response pty write on ENQ FIXED (was String::into_bytes() of the encoded text; DerivedConfig.enquiry_response is now decoded Vec<u8>; test enquiry_response_is_decoded_to_bytes)
command, initial-command (Command) exec argv/shell string, Windows CreateProcessW, flatpak ay, macOS login OK (decoded in command.rs config_str_to_bytes; Windows strict try_to_wide); window title from argv[0]/xdg-terminal-exec is FIXED (decoded text)
env (RepeatableStringMap) child environment OK (Unix config_str_to_os; Windows strict try_to_os)
input (RepeatableReadableIO) pty write; file read Raw: OK (decoded after clone_parsed). Path: FIXED, now typed EncodedOs (was opened undecoded); embedded initial_input escape works on bytes (string_escape) so parse_bytes(string_escape(b)) == b; test initial_input_escape_round_trips_bytes, readable_io_path_decodes_to_the_original_bytes
link regex onig regex over terminal text FIXED (decoded text; a pattern byte >0x7F that is not UTF-8 cannot match terminal text anyway)
working-directory, background-image, custom-shader, gtk-custom-css, bell-audio-path, config-file, theme filesystem OK (typed EncodedOs since Z7)
command-palette-entry title/description GTK palette labels FIXED (decoded text); its action follows the keybind row
keybind payloads: text, csi, esc pty FIXED (gap 9)
keybind search: search needle bytes FIXED (decoded; the GTK search entry encodes its plain UI text with valid_str_to_config_string)
keybind set_surface_title, set_tab_title SetTitle action FIXED (decoded text)
keybind activate_key_table* table-name lookup (config vs config) plain/consistent (both sides encoded)
keybind cursor_key pty not config-parsable (raw String built in code)
selection-word-chars codepoints plain
Diagnostic.{key,message} +validate-config stdout, +show-face stderr FIXED (print decoded bytes); GUI diagnostics are display text
CLI/C: ghostty_init argv, ghostty_config_load_file, surface options (working_directory, command, env_vars, initial_input), ghostty_surface_text, clipboard completion, binding action, ghostty_config_get/open_path OK (bytes in, encoded or bytes)
C key event text, preedit String (lossy) documented limit: UTF-8 text by the ghostty.h contract (IME/key text); would need a byte KeyEvent.utf8 across ghostty-input

3. OS data entering the system (byte preserving)

Source Entry point Status
process environment std::env::var_os / vars_os everywhere (get_env_map, getenv, homedir, xdg, shell_from_env, TMPDIR/TMP, GTK LANG, PATH); std::env::var remains only for ASCII-only developer switches (GHOSTTY_LOG, RUST_LOG, COLORFGBG, GHOSTTY_DEBUG_*) OK
argv os::args (args_os / C ghostty_init bytes) -> collect_config_strings OK
config files, theme files read as bytes -> encoded strings OK
passwd (getpwuid), uname bytes -> EncodedOs (hostname: text for comparison) OK
OSC 7 pwd, OSC 8 URIs, kitty file paths bytes kept (set_pwd(&[u8]), os_path_from_bytes) OK
GTK file drops os_bytes(path) into the shell-escaped paste OK
clipboard (C API, OSC 52, GTK text) bytes (&[u8] in complete_clipboard_request; GTK text is UTF-8) OK
D-Bus / flatpak cwd and argv ay bytes; environment is a{ss} (UTF-8 by D-Bus type) known limit
+new-window over D-Bus arguments are GVariant strings known limit (UTF-8 by type, as Zig)
Windows APIs WTF-16 via EncodedOs::{from_wide,to_wide}; process launch is strict OK

4. Typed where practical

exec::Config.term (EncodedOs), DerivedConfig.enquiry_response (Vec<u8>), ReadableIO::Path (EncodedOs), ghostty_apprt::CoreSurface::complete_clipboard_request (&[u8]). The remaining String config fields are either UI/plain text (above) or go through the generic config machinery (macro, formatter, C value) where a newtype would touch every field; each consumer is listed and decoded at its boundary instead.

5. Gate

cargo test --workspace, clippy --workspace --all-targets -D warnings, cargo +nightly fmt --all, just check-macos (both targets), just check-windows, just check-wasm, +show-config --default --docs vs /usr/bin/ghostty (only the gtk-opengl-debug line differs): see the final report. Windows and macOS code is compile-checked, not run natively.