- Limit d=a/A to non-virtual placements that intersect the active screen.
- Keep unrelated unplaced image data when processing d=A.
- Make d=R delete matching unused images even when they have no
placements, and default an omitted x bound to zero.
- Give ED2 a separate clear path that preserves scrollback references
while reclaiming every unreferenced image.
References:
- Spec:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images
- Delete reference implementation:
0ecb10d158/kitty/graphics.c (L2114-L2363)
- ED2 reference implementation:
0ecb10d158/kitty/screen.c (L2919-L2958)
Previously, delete left partial bytes alive for the next upload, while
failed or incomplete retransmissions kept stale placements visible.
Abort incomplete chunked image uploads on delete commands and remove an
existing image and its placements when retransmission of an explicit ID
begins.
Previously, delete left partial bytes alive for the next upload, while
failed or incomplete retransmissions kept stale placements visible.
Abort incomplete chunked image uploads on delete commands and remove an
existing image and its placements when retransmission of an explicit ID
begins.
File transmissions with exactly S bytes or trailing file data previously
failed because appendRemaining reports StreamTooLong when it reaches its
limit. This broke the protocol's partial-file transmission path.
Use an exact-length allocation and read when S is nonzero, rejecting
premature EOF and values above the image limit. Preserve the existing
bounded read-to-EOF behavior for S=0.
https://sw.kovidgoyal.net/kitty/graphics-protocol/#local-client
File transmissions with exactly S bytes or trailing file data previously
failed because appendRemaining reports StreamTooLong when it reaches its
limit. This broke the protocol's partial-file transmission path.
Use an exact-length allocation and read when S is nonzero, rejecting
premature EOF and values above the image limit. Preserve the existing
bounded read-to-EOF behavior for S=0.
https://sw.kovidgoyal.net/kitty/graphics-protocol/#local-client
Clamp X and Y offsets when a placement is created and normalize them
again against current cell geometry when sizing and rendering. The
protocol requires offsets to remain within the first cell and not
enlarge explicit c/r rectangles:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#controlling-displayed-image-layout
Previously, explicit placements were sized to the full c/r rectangle
before the offset was applied. This extended their far edge into
neighboring cells and let unbounded offsets reach renderer geometry.
Clamp X and Y offsets when a placement is created and normalize them
again against current cell geometry when sizing and rendering. The
protocol requires offsets to remain within the first cell and not
enlarge explicit c/r rectangles:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#controlling-displayed-image-layout
Previously, explicit placements were sized to the full c/r rectangle
before the offset was applied. This extended their far edge into
neighboring cells and let unbounded offsets reach renderer geometry.
Kitty graphics placements previously calculated pixel and grid geometry
from requested source dimensions before intersecting them with the
image. The renderer clamped explicit dimensions later but treated
omitted source dimensions as the full image.
This stretched clipped crops into incorrectly sized destinations and
exposed inconsistent geometry through storage, rendering, and
libghostty.
Resolve the source rectangle once against the image bounds and reuse it
for placement sizing, renderer preparation, and the C API. Add
regression tests for omitted and explicit dimensions and renderer
geometry.
Spec:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#controlling-displayed-image-layout
Reference:
0ecb10d158/kitty/graphics.c (L1225-L1240)
Reject Kitty graphics commands that specify both an image ID and an
image number across every protocol action.
These commands previously produced no wire response for transmissions,
while put and delete actions could proceed using one identifier and
mutate terminal state.
Retain the original command identifiers, validate them before action
dispatch, and preserve them in the EINVAL response. Add regression
coverage for every action, response encoding, quiet suppression, and
pre-mutation rejection.
https://sw.kovidgoyal.net/kitty/graphics-protocol/#requesting-image-ids-from-the-terminal
Kitty graphics placements previously calculated pixel and grid geometry
from requested source dimensions before intersecting them with the image.
The renderer clamped explicit dimensions later but treated omitted
source dimensions as the full image.
This stretched clipped crops into incorrectly sized destinations and
exposed inconsistent geometry through storage, rendering, and
libghostty.
Resolve the source rectangle once against the image bounds and reuse it
for placement sizing, renderer preparation, and the C API. Add
regression tests for omitted and explicit dimensions and renderer
geometry.
Spec: https://sw.kovidgoyal.net/kitty/graphics-protocol/#controlling-displayed-image-layout
Reference: 0ecb10d158/kitty/graphics.c (L1225-L1240)
Reject Kitty graphics commands that specify both an image ID and an
image number across every protocol action.
These commands previously produced no wire response for transmissions,
while put and delete actions could proceed using one identifier and
mutate terminal state.
Retain the original command identifiers, validate them before action
dispatch, and preserve them in the EINVAL response. Add regression
coverage for every action, response encoding, quiet suppression, and
pre-mutation rejection.
https://sw.kovidgoyal.net/kitty/graphics-protocol/#requesting-image-ids-from-the-terminal
Supersedes #13886
This fixes an incompatibility between Ghostty and Kitty 0.47.1+.
Previously, Ghostty handled `C=0` by moving down at most one terminal
height and then clamping the horizontal destination to the screen. The
limit protected against an image command requesting billions of rows,
but it counted ordinary movement to the bottom as well as scrolling.
Kitty defines `C=0` as leaving the cursor after the image and implements
the movement as rows minus one, plus one row when the image reaches the
right edge:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#controlling-displayed-image-layout0ecb10d158/kitty/graphics.c (L1256-L1260)0ecb10d158/kitty/screen.c (L1903-L1915)
(Had to view Kitty source to verify what the spec meant)
For example, an eight-row, full-width image in a five-row terminal needs
four moves to reach the bottom and four more to scroll. The old limit
allowed only five moves in total, so the next image started four rows
inside the first one. Clamping the horizontal destination also left the
cursor at the final column instead of wrapping to column one of the next
row. Consecutive images could therefore overlap in both directions.
Supersedes #13886
This fixes an incompatibility between Ghostty and Kitty 0.47.1+.
Previously, Ghostty handled `C=0` by moving down at most one terminal
height and then clamping the horizontal destination to the screen. The
limit protected against an image command requesting billions of rows,
but it counted ordinary movement to the bottom as well as scrolling.
Kitty defines `C=0` as leaving the cursor after the image and implements
the movement as rows minus one, plus one row when the image reaches the
right edge:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#controlling-displayed-image-layout0ecb10d158/kitty/graphics.c (L1256-L1260)0ecb10d158/kitty/screen.c (L1903-L1915)
(Had to view Kitty source to verify what the spec meant)
For example, an eight-row, full-width image in a five-row terminal needs
four moves to reach the bottom and four more to scroll. The old limit
allowed only five moves in total, so the next image started four rows
inside the first one. Clamping the horizontal destination also left the
cursor at the final column instead of wrapping to column one of the next
row. Consecutive images could therefore overlap in both directions.
Mode 2048 requires terminals to report the current rows, columns, and
pixel dimensions [when the mode is
enabled](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83#:~:text=When%20first%20enabled%2C%20the%20terminal%20MUST%20send%20a%20report%20of%20the%20current%20size.),
then report updated geometry after later resizes. The native termio
stream already queues the initial report, but the terminal stream only
reported from `resize`, so libghostty consumers that enabled the mode
after committing geometry received nothing until another resize.
This makes the generic handler match termio by requesting current
geometry through the existing size callback and writing the encoded
report through `write_pty` on every enable.
Add a snapshot decoder option that leaves continuation tracking enabled
on decoded terminals. This lets caller access the continuation bytes (if
any) that were applied to the terminal.
This lets replay callers export an unfinished parser or UTF-8 sequence
from the returned terminal.
This defaults to off.
Add a snapshot decoder option that leaves continuation tracking
enabled on decoded terminals. This lets caller access the continuation
bytes (if any) that were applied to the terminal.
This lets replay callers export an unfinished parser or UTF-8 sequence
from the returned terminal.
This defaults to off.
The GhosttyWriter adapter was unbuffered, so for streaming writers that
make small writes like the formatter, it produces a crazy amount of
callbacks: one styled HTML page invoked the callback ~50,000 times in a
benchmark lol. This has a particularly large impact on callers who are
supplying callbacks through an expensive FFI interface, like Go.
Change WriterAdapter to have an optional buffer (initBuffered) and use a
4 KB buffer for all current callers. Also optimize single byte splats to
use memset.
Results: that same styled HTML example goes from ~50K callbacks to 124.
And throughput through the C API also improved across every workload I
tested (styled and unstyled text in every format).
The GhosttyWriter adapter was unbuffered, so for streaming writers that
make small writes like the formatter, it produces a crazy amount of callbacks:
one styled HTML page invoked the callback ~50,000 times in a benchmark lol.
Change WriterAdapter to have an optional buffer (initBuffered) and use
a 4 KB buffer for all current callers. Also optimize single byte splats
to use memset.
Results: that same styled HTML example goes from ~50K callbacks to 124.
And throughput through the C API also improved across every workload
I tested (styled and unstyled text in every format).
This prevents us from pulling in test images that trigger some
anti-virus scanners. It's also smaller since it only has the necessary
bits that we need.
This also updates to the latest release: 0.4.0-alpha.10.
## Summary
- Prevent desktop notification title and body truncation from producing
invalid UTF-8.
- Truncate fixed-size buffers to the longest valid UTF-8 prefix.
- Add regression tests for multibyte characters at the buffer boundary.
Fixes#13795
## Testing
- Confirmed the original reproducer produces `[Invalid UTF-8]` with the
installed Ghostty.
- Confirmed the patched Ghostty displays a valid, truncated
notification.
- Added tests covering both notification title and body truncation.
## AI Usage Disclosure
I used OpenAI Codex to investigate the root cause, implement the fix and
regression tests, run validation, and help prepare the issue and PR
descriptions. I reviewed and understand the submitted change.
Store the Comparable ObjectIdentifier directly instead of wrapping the
only sort key type in AnySortKey.
The expected deterministic ordering of equal terminal command titles is
also now verified by a unit test.
Previously, formatting a cursor at the right edge emitted CUP, which
cleared pending wrap. Replayed output then overwrote the edge cell
instead of wrapping before the next printable character.
When pending wrap is set, move to and reformat the final cell to restore
the flag through normal VT behavior. Emit cursor pen state afterward and
cover replay plus pin-map behavior with a regression test.
Previously, formatting a cursor at the right edge emitted CUP, which
cleared pending wrap. Replayed output then overwrote the edge cell
instead of wrapping before the next printable character.
When pending wrap is set, move to and reformat the final cell to restore
the flag through normal VT behavior. Emit cursor pen state afterward and
cover replay plus pin-map behavior with a regression test.
Store the Comparable ObjectIdentifier directly instead of wrapping the
only sort key type in AnySortKey.
The expected deterministic ordering of equal terminal command titles is
also now verified by a unit test.
Remove the unused, callback-based detachable header implementation. It
supported the previous terminal inspector, which has since been deleted
(fdbe4343c). The current inspector uses DetachableHeader directly.
Remove the unused, callback-based detachable header implementation. It
supported the previous terminal inspector, which has since been deleted
(fdbe4343c). The current inspector uses DetachableHeader directly.
Continues the Belarusian (`be`) translation for v1.4 per #13766.
Translates the remaining 181 strings (mostly the newly-localized command
palette), bringing `be` to 253/253.
A custom memory pool for Wasm that grows by exactly one item size per
growth and shares the pool across the entire Wasm-module instead of
per-terminal.
Some background on why `std.heap.MemoryPool` is considered harmful for
WebAssembly:
First, the std.heap.MemoryPool grows 1.5x at each growth point. The
backing allocator for that is usually a GPA which is the BrkAllocator
for wasm. This grows by power-of-two big-allocation slots. If you pair
these together you get a massive permanent linear memory growth.
On non-wasm targets, the memory growth doesn't matter because these are
virtual memory mappings that don't cost physical memory, but wasm
doesn't work that way. Also on native targets, the syscalls to allocate
memory are very expensive (relatively), so it makes sense to allocate
large virtual memory chunks and avoid them. Again, wasm doesn't work
this way.
Second, we were using one pool per terminal. On wasm, this meant that we
paid for the free list N times. On non-wasm, this makes sense because
the synchronization overhead has so far been measurable enough under
load to be prohibitive (although, I'm still skeptical about this and
want to look into it). On wasm, we build single-threaded modules, so we
can use a global free list without any extra overhead.
## Benchmarks
80x24 terminal with 1000-line scrollack processing 16MB of plain ASCII.
| Scenario | Before | After |
| ------------------------------- | --------: | --------: |
| Fresh instance | 0.56 MiB | 0.56 MiB |
| First `terminal_new` (delta) | +3.44 MiB | +0.88 MiB |
| One filled terminal (total) | 4.00 MiB | 1.88 MiB |
| Each additional filled terminal | +3.00 MiB | +0.44 MiB |
| 5 filled terminals (total) | 16.00 MiB | 4.06 MiB |
Throughput numbers are unchanged on wasm and native (to be expected in
the latter because this is all gated on
wasm).
Note I'm still very much optimizing the above numbers! This is just my
first big win.
**AI usage:** None used except to validate and judge.
A custom memory pool for Wasm that grows by exactly one item size per
growth and shares the pool across the entire Wasm-module instead of
per-terminal.
Some background on why `std.heap.MemoryPool` is considered harmful for
WebAssembly:
First, the std.heap.MemoryPool grows 1.5x at each growth point. The backing
allocator for that is usually a GPA which is the BrkAllocator for wasm.
This grows by power-of-two big-allocation slots. If you pair these together
you get a massive permanent linear memory growth. On non-wasm targets,
this doesn't matter because these are virtual memory mappings that don't
cost physical memory, but wasm doesn't work that way.
Second, we were using one pool per terminal. On wasm, this meant that
we paid for the free list N times. On non-wasm, this makes sense because
the synchronization overhead has so far been measurable enough under
load to be prohibitive (although, I'm still skeptical about this and want
to look into it). On wasm, we build single-threaded modules, so we can use
a global free list without any extra overhead.
## Benchmarks
80x24 terminal with 1000-line scrollack processing 16MB of plain ASCII.
| Scenario | Before | After |
| ------------------------------- | --------: | --------: |
| Fresh instance | 0.56 MiB | 0.56 MiB |
| First `terminal_new` (delta) | +3.44 MiB | +0.88 MiB |
| One filled terminal (total) | 4.00 MiB | 1.88 MiB |
| Each additional filled terminal | +3.00 MiB | +0.44 MiB |
| 5 filled terminals (total) | 16.00 MiB | 4.06 MiB |
Throughput numbers are unchanged on wasm and native (to be expected in
the latter because this is all gated on wasm).
Zig default's Wasm stacks to 1MB. Change it to 128 KB instead.
This removes 896 KiB from every Wasm instance's initial linear memory
reservation. That means that simply _loading_ `ghostty-vt.wasm` is down
this much.
Through various workload benchmarks of real terminal snapshots,
artificial worst case full ascii, full styled, full emoji, full mixed,
etc. workloads, I wasn't able to get a stack to go above 17 KB, so 128
KB is VERY generous. Lets start here.
Zig default's Wasm stacks to 1MB. Change it to 128 KB instead.
This removes 896 KiB from every Wasm instance's initial linear memory
reservation. That means that simply _loading_ `ghostty-vt.wasm` is down
this much.
Through various workload benchmarks of real terminal snapshots,
artificial worst case full ascii, full styled, full emoji, full mixed,
etc. workloads, I wasn't able to get a stack to go above 17 KB, so 128 KB
is VERY generous. Lets start here.
- Drop redundant 'было' copula with short-form participles (lines 1767, 1769)
- Use short predicative form 'недаступна' (line 1780)
- 'у двух фарматах:' instead of dash-construction (line 156)
- 'Аднавіць' instead of 'Паўтарыць' for Redo (semantic pair with Undo)
- Fix 'у' → 'ў' after 'ANSI' (7 places)
- Align label/description wording for Split Zoom, Read-Only, Float on Top, Secure Input
- 'усе акны' instead of 'усе вокны' (consistent with 'акно')
- 'калі яна ёсць' instead of 'даступная' (if present ≠ available)
- Infinitive 'Дадаць' instead of imperative 'Дадайце'
Key events without a kitty entry fall back to writing their UTF-8 text
directly. On GTK, keys whose unshifted keysym is a dead key or level 5
latch have no unshifted codepoint and take this path. With event type
reporting enabled, releases therefore emitted the same text as presses
and duplicated characters in applications such as Neovim.
Skip the raw text fallback for release events while retaining it for
presses and repeats. Keep the guard in the shared encoder so release
events for identified keys still retain the UTF-8 data used to derive
alternate keys.
Cover releases with and without report-all mode, and verify that repeat
events continue to emit fallback text.
- https://github.com/ghostty-org/ghostty/discussions/12192
- https://github.com/ghostty-org/ghostty/discussions/12084
- https://github.com/ghostty-org/ghostty/discussions/12433
- https://github.com/ghostty-org/ghostty/discussions/13816
## Testing
- `zig build test-lib-vt -Dtarget=x86_64-linux-gnu`
- `zig build -Demit-lib-vt -Dtarget=x86_64-linux-gnu`
- `zig build`
- Verified the regression test fails without the release guard
- Manually tested the GTK backend under Wayland/Sway and X11/XWayland,
with the GTK simple input context and ibus 1.5.34:
- Ergo-L `!` and `'`
- Spanish `[`, `{`, `]`, and `}`
- Presses, repeats, and both modifier-release orders in `nvim --clean`
- Dead-key composition and cancellation
- Unicode hexadecimal input
- Full kitty keyboard mode with `kitty +kitten show_key -m kitty`,
including composed text
## AI disclosure
OpenAI Codex assisted with investigating the reports, reviewing the GTK
and kitty input paths, extending the regression tests, running
validation, and drafting this description. I reviewed the final code,
edited this description, manually performed the tests listed above, and
understand how the change interacts with the input encoder.
When loading a theme from a path that includes a tilde:
```
theme="~/.cache/wal/colors-ghostty"
```
ghostty currently fails with the following error:
```
cannot include path separators unless it is an absolute path
```
This PR tries to expand the ~ of the path. If there is no ~ or expansion
fails, it falls back to the unexpanded value.
Replace a bunch of type-specific Wasm allocation functions with a
generic byte allocator and reusable opaque out-parameters for pointers.
This makes it a lot more ergonomic (relatively) to use the Wasm
interface and removes a dozen or so exports.
This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.
This adds a test to verify this all works as intended and runs in CI.
Replace a bunch of type-specific Wasm allocation functions with a generic
byte allocator and reusable opaque out-parameters for pointers. This
makes it a lot more ergonomic (relatively) to use the Wasm interface
and removes a dozen or so exports.
This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.
This adds a test to verify this all works as intended and runs in CI.