Files
ghostty-rs/docs/rewrite/DEVIANT_FIXES.md
T

116 lines
25 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 out of scope)
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) |
| Development resources fallback | `resourcesDir` returns nothing when no terminfo bundle is found next to the binary | In the Cargo `dev` 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.