Files
ghostty-rs/docs/rewrite/DEVIANT_FIXES.md
T
ddidderr 85489a164e feat-deviant(os): embed a configurable resource directory
Relocating a Cargo-installed binary loses executable-relative resource
lookup even when themes and shell integration exist under /usr/share.
Allow packagers to set GHOSTTY_BUILD_RESOURCES_DIR when compiling.

Absolute paths are used directly; relative paths resolve against the
running executable's directory for relocatable installations. Explicit
paths do not require terminfo sentinels. A nonempty runtime override wins
in every profile when the build option is configured. Unset or empty
build options preserve existing discovery, including debug precedence.
Cargo watches the build option so changing or unsetting it recompiles
lookup. Resource files still need to be installed separately.

Document this requested upstream deviation and the build option in the
README, architecture overview, and deviation register.

Test Plan:
- Eight resource lookup unit tests pass.
- Nightly formatting and ghostty-os Clippy with -D warnings pass.
- Production build with /usr/share/ghostty succeeds; a copied executable
  lists all 463 themes with no runtime override.
- Compiled probes verify absolute, relative, unset, and empty options,
  Cargo rebuilds, relocation, runtime overrides, and unrelated cwd.

Deviation-Register: docs/rewrite/DEVIANT_FIXES.md
2026-10-13 00:07:15 +02:00

117 lines
26 KiB
Markdown

# Deviant fixes: where the Rust port intentionally differs from Ghostty 1.3.1
## The rule
The port is faithful to Ghostty 1.3.1 (including its quirks), with one
exception: genuine upstream bugs are **fixed, not replicated**. That means
memory-safety problems and undefined behaviour (use-after-free, stale
pointers, out-of-bounds reads, leaks), infinite loops, data loss or
corruption (lost or overwritten output), and crashes/asserts reachable from
input. See rule 2 in `AGENT_GUIDE.md`.
From now on every such fix is committed on its own as
`fix-deviant(<scope>): ...` and gets a row in the first table below. This
register also covers the deviations made **before** that convention; their
commits bundle other work, so the "Commit" column names the commit that
introduced the Rust behaviour (found with `git log -S` / `git blame`), not a
dedicated fix commit.
Conventions: Zig paths are relative to `ghostty/src/`. Line numbers refer to
the Zig tree used for the port. "unverified" means I could not confirm the
detail from the notes or code; "none" means no test exercises the deviation.
"uncommitted" means the change was in the working tree (agent Z17, ghostty-cli)
when this file was written.
## Upstream bugs fixed (behaviour deliberately differs)
| # | Area | Zig location | Ghostty behaviour | Rust behaviour | Commit | Test |
|---|------|--------------|-------------------|----------------|--------|------|
| 1 | Kitty graphics: placement pins | `terminal/kitty/graphics_storage.zig:148-176` (`addPlacement`), `:517-` (`evictImage`) | The tracked pin of a placement is leaked when its image is evicted and when an external placement id is reused (`getOrPut` then overwrite of the value). | `add_image`/`add_placement`/`set_limit` take the `PageList` and release the replaced/evicted placement's tracked pin. | d47408f | `storage_add_placement_replaces_and_releases_pin`, `storage_eviction_prefers_unused_then_oldest` (`ghostty-terminal/src/kitty/graphics_storage.rs`) |
| 2 | Kitty graphics: `Placement::rect` | `terminal/kitty/graphics_storage.zig:~789-798` (`grid_size.rows - 1`, `grid_size.cols - 1`) | Unsigned underflow for a 0-sized grid. | `saturating_sub(1)` on rows/cols. | d47408f | none |
| 3 | Kitty graphics: file medium `S=` | `terminal/kitty/graphics_image.zig:268-269` (`appendRemaining(.limited(size))`) | `StreamTooLong` whenever exactly `size` bytes are read, so any satisfiable `S>0` fails with InvalidData (Zig tests only use `S=0`). | Data is truncated to `min(S, 400MB)`, as the Kitty spec says. | d47408f (note clarified in 687752c) | `image_load_file_offset_and_size` (`ghostty-terminal/src/kitty/graphics_image.rs`) |
| 4 | Kitty graphics: zlib size boundary | `terminal/kitty/graphics_image.zig:412` (`.limited(max_size)`) | Decompression errors at exactly 400MB of output. | Errors only above 400MB (`take(MAX_SIZE + 1)` then `len > MAX_SIZE`). | d47408f | none |
| 5 | Renderer: link matching | `renderer/link.zig:99-123` (`defer offset = end`) | A zero-length regex match leaves `offset` unchanged, so the loop never terminates. | A zero-length match advances by one character. | 871c301 | none dedicated (`render_cell_map*` in `ghostty-renderer/src/link.rs` do not cover it) |
| 6 | Input: keybind formatter | `input/Binding.zig:2112-2150` (`Set.Value.formatEntries`) | The shared buffer is rewound to a fixed position after each sub-entry, which corrupts the prefix of sibling entries for chained leaves in sequences. | Each entry is formatted from its own prefix. | e034322 | `set_format_entries_sequence_shares_prefix` (`ghostty-input`) |
| 7 | Core: selection replacement | `Surface.zig:2310-2322` (`setSelection`) | `select()` untracks the previous selection's pins, then `sel.eql(prev)` compares through those released pins (stale pointers). | The comparison happens before the old selection is replaced. (Rust handles are checked, so the naive port panicked; the e2e tests found it.) | 2f58f02 | `select_all_twice_with_copy_on_select_does_not_panic` (`ghostty-core/tests/e2e_bindings.rs`), `drag_selects_and_copy_on_select_fills_selection_clipboard` (`e2e_mouse.rs`). Dedicated eql-before-replace test: unverified |
| 8 | Datastruct: `lru` eviction report | `datastruct/lru.zig:163-166` | `entry.data.key` is overwritten with the new key before `entry.data` is copied out as `evicted`, so the evicted key is wrong. | `GetOrPutResult.evicted` carries the old key and old value. | 5b8c92b | `get_or_put` checks only `evicted.value`; the key is not asserted (`ghostty-datastruct/src/lru.rs`) |
| 9 | Terminal: stale cell pointer in grapheme attach | `terminal/Terminal.zig:482` and `:539` | `prev.cell` survives a page reallocation (`increaseCapacity` in `printCell` of the spacer tail), so `appendGrapheme` writes into freed memory and the grapheme (for example U+FE0F) is lost. | Cells are addressed by index; the grapheme is kept. | 4eb03f6 | `difftest_upstream_uaf_grapheme_survives_page_growth` (`ghostty-terminal/src/stream_readonly.rs`); the difftest harness stores such mismatches as `suspect*` |
| 10 | Terminal: asserts and segfaults reachable from input | `Terminal.zig:375` (`assert(!unicode.graphemeBreak(cp1, cp2, &state))` after mode 2027 is toggled with U+2028/U+2060/BOM), `terminal/page.zig:1012` (`assert(y < size.rows)`; Devanagari conjunct at the right margin, including the bottom-row case where the margin wrap scrolls and `moveGrapheme(prev.cell, ...)` runs on a blank cell), `Terminal.zig:834` (`assert(!cell.hyperlink)`; REP with a huge count while a hyperlinked page grows) | Zig aborts (ReleaseSafe) or faults (ReleaseFast) in the margin case; the first and third abort only with assertions on (roughly 0.1-0.4 % of generated difftest cases). | No panic and no error. The grapheme replay ignores the break result like ReleaseFast (the assertion is dropped, because this repository's `release` profile enables debug assertions, so input could crash a release build). The margin case moves the grapheme from the cell that actually holds it (`prev.cell`, else the cell above) and never flags a cell without data. Verified with a Rust-only panic hunt (`difftest_fuzz panics`, debug build, about 60k cases, 0 panics) and the Zig differential run (0 divergences). Zig-aborting cases are excluded from the differential comparison. | 4eb03f6 (REP, first margin case); Z21 (uncommitted): assertion and bottom-row margin case | `difftest_grapheme_wrap_with_margins_moves_prev_cell`, `difftest_grapheme_wrap_scrolling_margins_keeps_grapheme`, `difftest_mode_2027_toggle_with_bom_does_not_panic`, `tools/difftest` (`ZIG CRASH` handling, `panics` mode) |
| 11 | VT: tmux control-mode ids | `terminal/tmux/control.zig:199,230,270,308` (`parseInt(usize) catch unreachable`) | An id that overflows `usize` hits `unreachable` (crash/UB). | The line is treated as a failed match. | b2b248e | none |
| 12 | VT: SGR separator bitset | `terminal/sgr.zig:228,514` (`params_sep.isSet(idx)` past `MAX_PARAMS`) | Asserts / out-of-range in `StaticBitSet` for long parameter lists. | Bounds-tolerant `sep_is_set`. | b2b248e | `sgr_underline_colon_with_trailing_separator_and_short_slice` (`ghostty-vt/src/sgr.rs`); whether Zig's path is reachable: unverified |
| 13 | OS: `NOTIFY_SOCKET` | `os/systemd.zig:100-116` | A leading `@` (abstract socket) is accepted by the check, then copied literally into the address, which cannot work. | A leading `@` becomes an abstract-namespace address. | f8c5ce0 | none (the notify tests use a path socket) |
| 14 | Renderer: GL `Buffer::init_fill` | `renderer/opengl/buffer.zig:62` | `len = data.len * @sizeOf(T)`, so `len` is in bytes while the rest of the type counts elements. | `len` is the element count. | 568ce78 | none |
| 15 | Renderer: `Pipeline` teardown | `renderer/opengl/Pipeline.zig:72-74` | `deinit` destroys only the program; the FBO and VAO leak. | Drop also deletes the FBO and VAO. | 568ce78 | none |
| 16 | Renderer: custom-shader uniforms | `renderer/generic.zig:2061-2064` (`cursor_text.color`, and the selection colours) | The union payload is read unconditionally; invalid for `cell-foreground` / `cell-background`. | Uniforms are set only for explicit colours. | 871c301 | none |
| 17 | Renderer (Metal): texture upload | `renderer/metal/Texture.zig` (`replaceRegion`) | The data length is not checked against the region. | A too-short slice is rejected (safe API). Zig line: unverified. | bab49ee | none |
| 18 | Input: binding order with catch-all | `input/Binding.zig:287-294` (sort key of `trigger.key`) | Reads the key of a catch-all trigger from the C union, which is undefined. | A catch-all sorts as 0. | e034322 | none |
| 19 | Font: unsupported FreeType pixel mode | `font/face/freetype.zig:707` | `@panic("unsupported pixel mode")`. | `Error::UnsupportedPixelMode`. | 2580176 | none |
| 20 | Font: OpenType SVG table | `font/opentype/svg.zig:45-46` | Reads `records[0]` and the last record unconditionally, even for an empty document list. | An empty list returns an error. | 2580176 | none |
| 21 | Termio (Windows): process thread handle | `Command.zig:347` (`CreateProcessW`) | The thread handle returned by `CreateProcessW` is never closed (leak). | It is closed. | 76771be | none (compile-checked only on Windows) |
| 22 | Termio (Windows): `focus_gained` | `termio/Exec.zig` (Windows branch; exact line unverified) | `@panic` on Windows. | No-op. | 76771be | none |
| 23 | GTK: close confirmation dialog (fallback build) | `apprt/gtk/class/close_confirmation_dialog.zig:45-46` | The fallback registers a signal named `close-request`, which already exists on `GtkWindow`; GObject aborts (dead code upstream). | The fallback names it `dialog-close-request`; the default build is unchanged. | f2252bb | runtime check recorded in `notes/compat-GC.md`; no unit test |
| 24 | libghostty: `ghostty_surface_inherited_config` | `apprt/embedded.zig:1573`, `:941` (`dupeZ` of the working directory) | Allocates a new copy per call and never frees it. | The strings are kept in the surface and freed with it. | 75bf604 | none |
| 25 | libghostty: invalid enum values from C | `apprt/embedded.zig` (mouse state/button, split direction, colour scheme, key action, pressure stage) | Undefined behaviour or safety panic. | Ignored / returns false; null handles are tolerated. | 75bf604 | unverified |
| 26 | Crash reporting: envelope attachments | `crash/sentry_envelope.zig:399-419` (`Attachment.encode`) | Never writes the `type` header, so the output cannot be parsed back. | Adds `"type":"attachment"` when absent. | b3145ad | `envelope_parse_multiple` (`ghostty-crash/src/envelope.rs`) is the nearest test; a dedicated test: unverified |
| 27 | Build data: webgen output | `build/webgen/main_actions.zig:5-6`, `main_commands.zig:6`, `main_config.zig:6` | A 2048-byte stdout buffer that is never flushed, so the generated website files are truncated. | The complete files are emitted. | e4cdd8e | none (checked byte-identical to Zig output of a patched copy, per `final-Z4.md`) |
| 28 | CLI: `+list-colors` plain output | `cli/list_colors.zig:59-72` | 4096-byte buffer, never flushed; piped output stops midway through `PeachPuff3` (12,297 bytes). | Flushed (15,434 bytes). | 6286e02 | none; found by RV11 (`review-rv11-codex.md`) |
| 29 | CLI: `+list-themes` diagnostics | `cli/list_themes.zig:124-131,171` | The messages ("Could not find the Ghostty resources directory", "No themes found...") go to a 4096-byte stderr buffer that is never flushed, so they are lost. | Flushed when the action ends. | `410d8fc` fix-deviant(cli) | `zig_writer::tests::unflushed_buffer_is_flushed_on_drop` |
| 30 | CLI: stdout as a regular file | `std.fs.File.writer` (Zig 0.15.2) as used in `cli/show_face.zig:68-73`, `cli/help.zig:34`, `cli/validate_config.zig:43-46`, `cli/show_config.zig:84` | The writer `pwrite`s at its own offset starting at 0: with stdout redirected to a file, output overwrites itself and earlier file content, and `Writer.end` truncates. | Writes sequentially at the inherited descriptor's current offset (no `pwrite` from offset 0, so earlier content and the writers' own output are not overwritten; this appends only when the descriptor is opened for append, e.g. `>>`); no truncation. | `410d8fc` fix-deviant(cli) (append behaviour first in `f76179f`) | `zig_writer::tests::regular_file_output_is_complete_and_appended` |
| 31 | GTK: `+new-window` D-Bus arguments | `apprt/gtk/ipc/new_window.zig:49-53` (`Bytes.new(ptr, len + 1)` + `Variant.newFromBytes(..., trusted = true)`) | Arguments are passed as trusted GVariant strings without validation; invalid UTF-8 or an interior NUL makes GIO abort the process while serializing (input-triggered crash). | `arguments_variant` validates first and returns `Error::IpcFailed` (message on stderr, no D-Bus call); valid arguments are byte-identical to Zig. | `65dd9d5` fix-deviant(gtk) | `ipc::new_window::tests::invalid_dbus_strings_are_rejected_without_abort` (`ghostty-apprt-gtk`) |
| 32 | OS: URL opener child reaping | `os/open.zig` (`try collectOutput(...)` before `wait()`) | When collecting the opener's output fails (e.g. more than 50 KiB on a stream), the error returns before `wait()`, leaving the child as a zombie. | The child is always reaped; the collection error is still logged. | `4a55abf` fix-deviant(os) | `ghostty-os` open tests |
| 33 | OS: maximum-length `NOTIFY_SOCKET` | `os/systemd.zig:100-116` (`socket_address.path.len < socket_path.len`, then `path[socket_path.len] = 0`) | A 108-byte path (the size of `sockaddr_un.path`) passes the length check, and the terminating NUL is written at `path[108]`, one byte past the array (out-of-bounds write). Separate from row 13 (the abstract-socket `@` handling). | The address is built with rustix's `SocketAddrUnix::new`, which takes the 108 bytes without a terminator (valid for Linux `AF_UNIX`) and rejects longer paths; no out-of-bounds write. Verified in code. | f8c5ce0 (`git log -S` on `ghostty-os/src/systemd.rs`: the only commit touching it, besides formatting/macOS changes) | `notify_maximum_length_socket_path_is_rejected_safely` (`ghostty-os/src/systemd.rs`) |
| 34 | Renderer (Metal): buffer reallocation size | `renderer/metal/buffer.zig:89` (`Buffer.sync`) and `:146` (`syncFromArrayLists`) | `size = req_bytes * 2` is already in bytes, then `size * @sizeOf(T)` is passed to `newBufferWithLength:options:`, so the new buffer is `sizeof(T)` times larger than intended (GPU-memory over-allocation). | The new buffer is exactly `2 * req_bytes` (`metal_layout::grown_buffer_bytes`); the element count is `2 * total_len`. | bab49ee; registered + test in `2040677` fix-deviant(renderer) | `metal_layout::tests::grown_buffer_bytes_is_double_the_required_bytes_not_scaled_by_size_of_t` (runs on any host; the Metal buffer itself is compile-checked only) |
| 35 | Termio: PTY hangup drops unread output | `termio/Exec.zig` `ReadThread.threadMainPosix` (exits as soon as `POLL.HUP` is seen) | `POLLIN \| POLLHUP` can arrive together with unread final output (a Linux PTY check confirmed this); exiting immediately loses the tail of the child's output. | After HUP the read thread makes one more pass over the fd, so the remaining output is delivered before exit. | 6629fb0 | none (needs a live PTY; verified by RV18 on Linux) |
| 36 | Font (wasm/browser Canvas): atlas pixel format | `font/face/web_canvas.zig` (atlas write branch on `.rgba`) | The branch tests a stale `.rgba` format against an enum that contains `.bgra`, so BGRA atlases are never converted. | The BGRA atlas conversion is implemented. No claim of equivalence with a real browser (not testable here). | d89691b | `ghostty-font` web_canvas tests (compile/Node level) |
| 37 | Termio: subprocess cleanup | `termio/Exec.zig:1188-1224` (`getpgid`) | Loops forever while `getpgid(child) == our pgid`, i.e. also when the child died before `setsid` and is an unreaped zombie (its pgid stays ours), hanging the surface close. | Still waits without a cap (no 5 s limit) while the child is alive, but stops (no SIGHUP, nothing to kill) once `waitid(WNOWAIT)` shows the child exited. | `b9ad595` fix-deviant(termio) | `getpgid_gives_up_on_exited_child_without_setsid`, `getpgid_waits_for_late_setsid` (`ghostty-termio/src/exec.rs`) |
| 38 | Config (Windows): absolute theme paths | `config/Config.zig:9788-9799` (`Theme.parseCLI`) | Any value containing `,`, `=` or `:` is parsed as a `light:..,dark:..` pair, and a Windows absolute path (`C:\\themes\\x`, `\\\\?\\C:\\...`) always contains a colon, so `theme = <absolute path>` is rejected on Windows (found by running the config tests under Wine). | On Windows a value that starts like an absolute Windows path (drive letter + `:` + separator, or `\\\\`) is a single theme; the pair syntax is unchanged (quote backslashes). Other platforms are unchanged. | `97a262d` fix-deviant(config) | `windows_absolute_path_detection`, `theme_windows_absolute_path_and_pair_with_paths` (`ghostty-config/src/types_b.rs`), `theme_loading` under Wine |
| 39 | Termio (Windows): bare program names | `Command.zig:261,347-358` (`startWindows`, `CreateProcessW(application_w, ...)`) with `termio/Exec.zig:1004` (`.path = self.args[0]`) | `lpApplicationName` is never searched on `PATH` (a partial name only completes against the current directory), so a `direct:` command / `-e` argv[0] such as `cmd.exe` fails with `FileNotFound`; only the default shell works (it is wrapped as `%WINDIR%\\System32\\cmd.exe /C ...`). | A name without drive/separator is looked up on `PATH` (`.exe` implied) before `CreateProcessW`; failures keep the original name (same error as Zig). | `4d7ad7c` fix-deviant(termio) | `resolve_application_searches_path_for_bare_names`, `bare_program_name_starts_via_path_search` (`ghostty-termio/src/command_windows.rs`), `echo_output_reaches_terminal_and_exit_is_reported` (`tests/exec_windows.rs`, Wine) |
## Upstream quirks deliberately kept (not fixed)
These look odd but are not memory-safety, loop, corruption or crash bugs, so
the port matches Ghostty.
- `shadertoy::Uniforms`: the selection foreground/background fields are swapped relative to the GLSL block. Both are `vec3`, so offsets agree (`ghostty-renderer/src/shadertoy.rs:878`; R1 note).
- Kitty storage: `delete range` uses `||` (matches everything); eviction stops only when `evicted > req`; `set_limit(0)` resets storage to defaults; screen reset also resets `total_limit`.
- `lru::get` does not change LRU order. `Tabstops::unset` XORs.
- `Screen`/keybind docs: `format_entry_docs` emits a blank line only (the `@tagName` lookup never matches).
- `Search`: an empty needle underflows exactly as in Zig, so the needle must be non-empty.
- i18n: the `strlen == 7` guard skips the tables for 8-byte locale names.
- CSI: `CSI K` accepts 0..2 only; ANSI DECRQM is unimplemented; unknown `many keycode` media keys stay a TODO.
- libghostty C header: `chostty_ipc_target_s` typo kept; `ghostty_inspector_metal_shutdown` is `bool` in the header but `void` in Zig (we follow Zig).
- Braille sprites: `debug_assert!` where Zig asserts, so odd cell sizes (for example 14x23) trip it in builds with debug assertions, as in Zig.
- Windows: no file locking in the ssh-cache `DiskCache` (Zig does not lock on Windows either).
- Config theme-name basename and numeric parsing follow Zig 0.15.2, not 0.16 (`fix(config)` commits 3068e3a, a57ff41).
## Other deliberate deviations (platform/Rust-imposed or explicitly requested)
Only entries with an observable behavioural difference are listed.
| Area | Zig/Ghostty behaviour | Rust behaviour | Where | Commit |
|------|-----------------------|----------------|-------|--------|
| Swift macOS app | Ships `macos/` | Not ported; the Rust side provides the `libghostty` C API, Metal/CoreText code, XCFramework script. Only compile-checked (`just check-macos`), never run (no macOS here). | `ARCHITECTURE.md` (Out of scope), `notes/macos-M1*.md`, `final-Z4.md` | bab49ee, 75bf604, 231ee35 |
| Windows (and wasm natively) | Native targets | Windows is compile-checked only (`just check-windows`), never run. wasm is built and its Node smoke/ABI tests run (`just check-wasm`, `scripts/wasm-check.sh`), but not in a real browser. | `notes/windows-WIN.md`, `notes/wasm-W1.md` | 76771be, d89691b |
| Crash reporting | Sentry with Breakpad on macOS, `SIGSEGV` handlers, local DB/cache, sessions, init thread | In-process reporter: `.ghosttycrash` envelopes in `$XDG_STATE_HOME/ghostty/crash`, panic hook and signal handler, minidumps on macOS/Windows. No Sentry DB/cache dir, no sessions, no init thread; reports are written on any panic; the crash dir is resolved at init; the signal path writes no attachment. | `notes/crash-CR.md`, `ARCHITECTURE.md` (Scope changes) | b3145ad, 72bfac1 |
| Surface copy-on-select on release | `setSelection` returns early when the new selection equals the old one | Left-release copies directly via `copy_on_select`, because the early return would never copy | `notes/ghostty-core.md` | ad76ae2 |
| Locking / threading | Renderer mutex held while sending mailbox messages and running mouse reports (`.locked` sends); `process_links` called unlocked | Reports, IO messages and runtime actions are collected and run after the lock is released; `process_links` takes the lock itself; termio overflow goes to an ordered outbox | `notes/ghostty-core.md`, `notes/ghostty-termio-IO*.md` | 6629fb0, ad76ae2 |
| Rendering of overlay / images | z2d overlay, wuffs image decoding, glslang+SPIR-V+spirv-cross | Direct RGBA overlay with approximate hairline placement; `png`/`jpeg-decoder`; `naga` GLSL front end (narrower than glslang, with a sampler2D helper rewrite) | `notes/ghostty-renderer-R1.md`, `R2.md`, `gaps-X2.md`; `just visual-parity` reports 0 differing pixels for the tested scenes | 568ce78, 871c301, f68b11d |
| Oniguruma | Native regex library for links and tmux | Hand-written matchers for the tmux parser; link regexes use the minimal `ghostty-onig` FFI crate | `notes/ghostty-vt-W2.md` | b2b248e, 2aa2a12 |
| zlib / PNG / base64 / shm | zig std, wuffs, simdutf, mmap | `flate2`, `png`, `base64` crate (ASCII whitespace ignored), `rustix` shm reads without mmap | `notes/ghostty-terminal-kitty.md`, `notes/ghostty-vt-W2.md` | d47408f, b2b248e |
| VT parser buffer | Fixed 2048-byte buffer with a reserved NUL | Growing `Vec<u8>` with a cap that models the reserved-NUL capacity; no observable difference is known (internal representation only) | `notes/ghostty-vt-W1.md` | b2b248e |
| CSI W logging | Logs a spurious "invalid cursor tabulation control" after `CSI 2 W` / `CSI 5 W` | No log line (log-only) | `notes/ghostty-vt-stream.md` | 47b479d |
| Searching | `CircBuf` + overlap buffer, libxev thread | `VecDeque`, `crossbeam-channel`, 24 ms refresh timer; identical results, assertions about buffer layout dropped | `notes/ghostty-terminal-search-tmux.md` | 8e9be42 |
| Config strings | UTF-8 not required in many places | Escaped-bytes representation (`EncodedOs`) so non-UTF-8 survives; behaviour and error names matched to Zig (for example `theme=/etc/passwd/x` gives `error.NotDir`, checked against the oracle) | `notes/review-rv11-codex.md` (item 3) | 672e8a4, 4bbe0d7 |
| `+version` / build info | Real build metadata | Placeholder `build_info` values in places; `gtk-opengl-debug` follows `cfg!(debug_assertions)` (the `release` profile has them on, `production` off) | `notes/ghostty-cli.md`, `notes/ghostty-config.md` | 6286e02 |
| OSC 52 base64 acceptance | Surface.zig:2149,2163 rules | Was different in both directions (Codex core audit); fixed to match Zig | `notes/review-core-codex.md` | 2f58f02 |
| Benchmark / synthetic tooling extensions | `ghostty-bench` has no `--time` option and only the simplified terminal handler; `ghostty-synthetic` has the actions `ascii`, `osc`, `utf8` | `ghostty-bench --time` prints the measured step time to stderr; `ghostty-bench --handler=full` runs the full `stream_readonly` handler instead of the print-only one; `ghostty-synthetic +sgr` generates SGR-heavy output. Dev/benchmark tooling only; otherwise upstream behaviour is unchanged | `crates/ghostty-bench/src/main.rs`, `src/benches.rs`, `src/bin/ghostty-synthetic.rs`; `notes/review-rv16-codex.md` | 0c4f0ee |
| Inspector font rasterizer (Windows, Apple, wasm) | Dear ImGui is built with `IMGUI_ENABLE_FREETYPE` (`pkg/dcimgui`), so the 32 px inspector font is rasterized by FreeType | Linux and other Unix targets: FreeType (`dear-imgui-rs` feature `freetype`, system FreeType through pkg-config), 32 px. Windows, Apple and wasm32 keep Dear ImGui's stb_truetype loader at 32 px, because `dear-imgui-sys` can only locate FreeType through pkg-config/vcpkg and those targets cannot be built or run here. Glyph anti-aliasing differs slightly there. | `crates/ghostty-inspector/Cargo.toml`, `src/inspector.rs` (`embedded_font_source`) | Z20 (uncommitted) |
| Build-time resource directory | No build-time resource path; relocated binaries need executable-relative terminfo detection or the runtime environment override | Optional `GHOSTTY_BUILD_RESOURCES_DIR` embeds an absolute path or a path relative to the executable directory. A nonempty runtime override wins in every profile when configured, then the build path; unset/empty retains existing discovery. No terminfo sentinel is required for an explicit path. User-requested packaging extension. | `crates/ghostty-os/src/resourcesdir.rs`, `crates/ghostty-os/build.rs`; tests `configured_absolute_dir_overrides_discovery_without_requiring_exe`, `configured_relative_dir_is_relative_to_executable`, `runtime_env_overrides_configured_dir_in_every_profile` | This commit (feat-deviant(os): embed a configurable resource directory) |
| Development resources fallback | `resourcesDir` returns nothing when no terminfo bundle is found next to the binary | In any Cargo profile, if neither installed resources nor `GHOSTTY_RESOURCES_DIR` apply, the in-repo `<workspace>/resources` directory is used when the binary lives inside this workspace | `crates/ghostty-os/src/resourcesdir.rs` (`dev_resources_dir`) | 967e3a1 |
## Maintenance
When you fix a genuine upstream bug, commit it alone as
`fix-deviant(<scope>): ...` and add a row to the first table with the hash,
the Zig file:line and the test. Replace "uncommitted" entries with hashes once
the Z17 work is committed.