Specifics in each commit message. This will be part of a security
advisory in 1.4.0 since these patches issues related to overflows, DoS,
unbounded memory allocation, etc.
The long-preedit fallback introduced in e95b1707c intentionally
allocated twice. The encoder wrote into an oversized caller-owned buffer
and returned only the written subslice, so transferring it required
manually shrinking the allocation or tracking its original capacity. The
copy kept that rare path simple.
The key encoder moved to std.Io.Writer.Allocating in 44496df899. Its
toOwnedSlice method handles shrinking and ownership transfer, remapping
when the allocator supports it and falling back to an allocation and
copy when it does not. Use it directly for WriteReq.alloc to remove the
guaranteed second allocation while preserving cleanup on failure.
Reclaim pin-backed Kitty graphics placements after their tracked screen
content is pruned. Treat garbage pins as non-renderable until the next
placement command sweeps them.
Placements that scrolled beyond retained history previously remained in
the placement map and tracked-pin set. Long-running graphics output could
accumulate stale state, and remapped garbage pins could appear at an
unrelated fallback location.
Sweep garbage placements before growing the placement map, releasing each
tracked pin while preserving virtual placements. Return no geometry or
visible render position for garbage pins and cover both storage and C API
behavior with regression tests.
Release a Kitty graphics placement's tracked pin before replacement.
Repeated updates to an external placement previously leaked tracked pins.
Pass the owning screen to storage and deinitialize the old placement.
Limit individual allocator requests made by PNG decoders to the Kitty
graphics protocol's 400 MiB image ceiling. Add a reusable allocator
wrapper for callers that need per-request bounds.
PNG decoding previously used Wuffs' 4 GiB package limit and checked
the result only after allocation. A tiny PNG with oversized dimensions
could cause a multi-gigabyte RSS spike before being rejected.
Wrap decoder allocators with LimitedAllocator and translate limit
rejections to invalid image data while preserving genuine out-of-memory
errors. Add allocator boundary tests and regression coverage for a
crafted PNG below Wuffs' limit.
Validate Kitty shared memory byte ranges before mapping and copying
image data. Interpret S as a byte count from O and preserve default
raw-image sizing.
Shared memory transmissions previously multiplied untrusted u32
dimensions before the limit check and sliced mappings with an unchecked
offset. Malformed commands could panic in safe builds or request a
wrapped allocation in fast builds.
Reject oversized dimensions before widening size arithmetic, derive
bounded ranges from the stat size, and enforce max_size before
constructing a slice. Add regression tests for explicit and implicit
offsets, out-of-bounds offsets, and maximum dimensions.
Validate Kitty file transmissions against a canonical path derived from
the open file handle. Keep temporary file policy and cleanup keyed to
that handle path.
Path validation previously ran before opening, so a local cooperating
process could replace a symlink or directory entry and make Ghostty
read a blocklisted file.
Open the submitted path once, derive its canonical path from the handle,
and use the same handle for stat and reads. Add a regression test that
replaces a blocked symlink after open and verifies the pinned target is
still rejected.
The long-preedit fallback introduced in e95b1707c intentionally allocated
twice. The encoder wrote into an oversized caller-owned buffer and returned
only the written subslice, so transferring it required manually shrinking
the allocation or tracking its original capacity. The copy kept that rare
path simple.
The key encoder moved to std.Io.Writer.Allocating in 44496df899. Its
toOwnedSlice method handles shrinking and ownership transfer, remapping when
the allocator supports it and falling back to an allocation and copy when it
does not. Use it directly for WriteReq.alloc to remove the guaranteed second
allocation while preserving cleanup on failure.
Treat Kitty placement dimensions and offsets as untrusted values when
calculating pixel, grid, and rectangle geometry. Saturate results that
do not fit and return no rectangle when missing pixel metrics produces
an empty grid.
Unchecked u32 arithmetic previously panicked in safe builds and wrapped
in fast builds. A zero row count could underflow into a maximum-size
page traversal, while maximum dimensions could spin cursor movement or
overflow render visibility calculations.
Use checked integer scaling instead of floating-point casts, saturating
arithmetic for extents and cursor columns, and bound off-screen cursor
work to the terminal row count. Compute C API visibility in i64 and
cover maximum protocol values in storage, execution, and render-info
tests.
Require temporary image file paths to match complete directory
components when checking /tmp, /dev/shm, the configured temporary
directory, and its resolved path.
The previous byte-prefix checks accepted similarly named sibling
directories such as /tmpX. A temporary-file transmission could read
and unlink a file outside the permitted temporary directories.
Add a component-boundary helper and regression coverage for built-in
and configured directory prefixes. An integration test also verifies
that a rejected file remains on disk.
Use inclusive image ID bounds for the Kitty graphics protocol range
delete operation.
Range deletion previously joined the lower and upper bound checks with or,
which matched every placement for any valid range. A targeted delete could
therefore remove every graphics placement.
Join the bounds with and and update the lowercase and uppercase range tests
to keep placements below and above the selected interval.
`printRepeat` (CSI `b`, repeat the previous character N times) calls
`print()` once per repeat, so something like `\x1b[2000b` ran grapheme
checks, width lookups, wrap handling, and the integrity assert 2000
times for what is usually the same character on the same row.
`Terminal.print` was 24% of samples on a REP-heavy micro benchmark.
This PR just aims to add a fast path by introducing a chunking
mechanism. anything that needs care (insert mode, grapheme clustering,
hyperlinks) still falls back to per-codepoint print() inside printSlice,
so behavior *should* stay unchanged.
Some profiling data:
Generated with some plain stupid logic:
```py
D = "benchdata"
parts, total = [], 0
while total < 40_000_000:
line = "x" + "\x1b[80b" + "y" + "\x1b[35b" + "\r\n"
parts.append(line); total += len(line)
open(f"{D}/rep.bin", "wb").write("".join(parts).encode())
```
**macOS (hyperfine, 15 runs, warmup 3):**
| | mean |
|---|---|
| before | 2.360 s |
| after | 1.166 s |
And now the really interesting and promising stuff
**Linux, 24-core NixOS x86_64 (poop, 6s sampling):**
| | wall_time | instructions | branch_misses | peak_rss |
|---|---|---|---|---|
| before | 1.51 s | 50.9 G | 9.41 M | 6.82 MB |
| after | 562 ms | 9.07 G | 114 K | 6.74 MB |
While doing some work on my tmux fork I noticed multiple parts of
libghostty-vt was slower than tmux equivalents(isolated). Turns out they
do some smart stuff there.
printRepeat called print() once per repeat, so something like \x1b[2000b
ran grapheme checks, width lookups, wrap handling, etc etc 2000 times.
printSlice is already documented as semantically identical to
calling print per codepoint, so this just feeds the repeated
codepoint through it in 4096-entry stack chunks. Simple runs take
the batched fast path, and anything that needs care falls back to the
previous behaviour.
Fixes#13614
Only translate the shared default commands when building the GTK
runtime. macOS now use the source strings until we do broader
localization.
Fixes#13614
Only translate the shared default commands when building the GTK runtime.
macOS now use the source strings until we do broader localization.
Fix for Issue #12940
I actually do not know if this has already been resolved and the issue
is just still open. Either way, here's a fix. Now we run a check to see
if the current program is accepting mouse events before discarding the
middle click.
Fixes#13386, based on
https://github.com/mustafa0x/ghostty/commit/a8c090
Defer transparent-titlebar KVO rebinding to the next main-queue turn.
Track the observed tab group so unchanged bindings are preserved.
Previously, a tab-group callback could invalidate and recreate its own
observation before returning, leaving closed terminal windows registered
with AppKit after the undo timeout. These windows accumulated titlebar
and layer state, increasing memory use and WindowServer CPU with tab
churn.
Validated with an AppDelegate change that sat and created/closed tabs in
a loop, then counted weak controllers/windows/nsapp window.
Most obvious next step in translating Ghostty is the command palette.
Added support for i18n.N_ (https://docs.gtk.org/glib/i18n.html#macros).
Made a Latvian translation for the command palette to test. Codex did
bulk of the translations but I verified them.
Fixes#13386
Defer transparent-titlebar KVO rebinding to the next main-queue turn.
Track the observed tab group so unchanged bindings are preserved.
Previously, a tab-group callback could invalidate and recreate its own
observation before returning, leaving closed terminal windows registered
with AppKit after the undo timeout. These windows accumulated titlebar and
layer state, increasing memory use and WindowServer CPU with tab churn.
Validated with an AppDelegate change that sat and created/closed tabs
in a loop, then counted weak controllers/windows/nsapp window.
Co-authored-by: Mustafa J <mustafa.0x@gmail.com>
the original wording is a bit confusing; I thought cursor-click-to-move
required shell-integration to be enabled, and was confused when the
mouse was still moving my cursor in fish even with
shell-integration=none.
#11799
Creating a CGEventTap without Accessibility permission leaks a Mach port
inside CoreGraphics on every failed attempt. The global keybind listener
retried this once per second while waiting for permission, so Ghostty
eventually exhausted the process port limit.
Request Accessibility access once, poll AXIsProcessTrusted while access
is denied, and create the event tap only after access is granted. Stop
polling before creation so an unrelated tap failure cannot restart the
leaking retry loop.
Tested this with various settings and global keys working fine.
This PR speeds up our formatting (plain text, html, and VT) by anywhere
from ~1.5x to ~8x.
The formatter is the hot path behind multiple features in Ghostty GUI:
clipboard copy (plain/VT/HTML), `write_screen_file`, `selectionString`,
and terminal search sliding window. It's also the hot path for
libghostty users, namely people like
[zmx](https://github.com/neurosnap/zmx) which utilize the VT formatter
to restore a terminal.
This PR also adds the benchmarking infrastructure for the formatter.
## How
- **Fast cell-run optimization.** For simple cells (single codepoint, no
style/hyperlink) we encode them as a single run rather than one at a
time.
- **Make some arguments comptime.** Generates more code but benchmarks
show it improves things, specifically for per-format switches that we do
a LOT.
- **Interned style id fast path.** Styles are interned per page, so id
equality implies style equality. We track the id of the active style and
skip the per-cell `Style` copy + `eql` when it matches.
- **Fast printing.** Avoid `std.fmt` where possible and assemble
integers, RGB colors, codepoints in fixed-width buffers with a single
memcpy. This was extracted partially to `fastprint.zig` so we can reuse
it.
- **Avoid double-formatting for tracked pins.** Previously we formatted
twice (once through a `Discarding` writer to count bytes) for pin maps.
Now I'm smarter about it and do a single pass.
## Performance
All on my machine, 80x24 terminal, 10K lines of scrollback.
| workload | main | this PR | speedup | throughput |
| --------------------- | -------- | -------- | ------- | ---------- |
| plain / plain | 5.74 ms | 1.67 ms | 3.4x | 364 MB/s |
| plain / vt | 6.46 ms | 1.04 ms | 6.2x | 596 MB/s |
| plain / html | 7.39 ms | 2.32 ms | 3.2x | 308 MB/s |
| unicode / plain | 9.74 ms | 5.42 ms | 1.8x | 276 MB/s |
| unicode / vt | 10.42 ms | 5.53 ms | 1.9x | 275 MB/s |
| unicode / html | 12.53 ms | 7.35 ms | 1.7x | 509 MB/s |
| styled / plain | 5.65 ms | 1.69 ms | 3.4x | 360 MB/s |
| styled / vt | 9.07 ms | 4.20 ms | 2.2x | 409 MB/s |
| styled / html | 10.78 ms | 6.64 ms | 1.6x | 740 MB/s |
| mixed / plain | 8.59 ms | 4.81 ms | 1.8x | 226 MB/s |
| mixed / vt | 11.25 ms | 6.65 ms | 1.7x | 250 MB/s |
| mixed / html | 14.47 ms | 10.52 ms | 1.4x | 414 MB/s |
| wrapped / plain | 7.30 ms | 1.11 ms | 6.6x | 733 MB/s |
| wrapped / vt | 8.14 ms | 1.04 ms | 7.8x | 789 MB/s |
| wrapped / html | 9.00 ms | 2.12 ms | 4.2x | 465 MB/s |
| pin-map / plain | 12.51 ms | 3.80 ms | 3.3x | |
| pin-map / vt | 13.12 ms | 2.99 ms | 4.4x | |
| active screen / plain | 12.5 µs | 2.7 µs | 4.6x | |
| active screen / vt | 18.4 µs | 6.8 µs | 2.7x | |
Workloads:
- `plain` is ASCII lines
- `unicode` is 2/3/4-byte codepoints with 10% grapheme clusters
- `styled` is heavy SGR churn
- `mixed` is styles + Unicode + hyperlinks
- `wrapped` is a continuous soft-wrapped stream
- `pin-map`/`active screen` are the selectionString/search-style and
visible-screen-only cases respectively.
Finally, what was previously thought impossible, is now possible.
The blur region itself is far more accurate than what we can conjure up
on our own, and in a much more finetuned and detailed way too.
Thank you, GTK devs!
Closes#13581