Fixes#13940
Various operations like scroll, line insert, erase, etc. operations
would clear cells but remain row metadata such as wrap flags and
semantic prompt state.
When we had fast paths in `grow` and other places like resize
we would adopt that without knowing it because we didn't properly clear.
Fix this by properly clearing row metadata too at the points where we
erase a row that might be reused.
Benchmarked with terminal-stream workloads for each affected path.
The added cost is 2-4 instructions per recycled row next to the existing
full-row cell clear. There was no measurable wall-clock change on any
workload.
AI helped run the benchmarks for me and analyze for missing places (it found
some!) but otherwise this was hand-designed.
Validate decoded OSC 5522 metadata, read MIME lists, and alias lists as
UTF-8. Treat an alias without a target MIME type as an invalid write
packet.
Malformed write packets now return EINVAL and terminate the in-flight
transaction instead of leaving it active. Malformed reads are dropped
without disturbing an active write.
Latest changes upstream to spec:
458421af46
Update OSC 5522 writes to reject every transaction that exceeds the
configured decoded-data limit. The previous behavior truncated text
while rejecting only non-text data.
Programs now receive EFBIG as soon as a write crosses the limit. The
clipboard remains untouched, and remaining write packets are ignored
until a new transaction begins. Raise the default to the protocol
minimum of 64 MiB.
This applies the latest spec change:
32ea104192
Validate decoded OSC 5522 metadata, read MIME lists, and alias
lists as UTF-8. Treat an alias without a target MIME type as an
invalid write packet.
Malformed write packets now return EINVAL and terminate the in-flight
transaction instead of leaving it active. Malformed reads are dropped
without disturbing an active write.
Latest changes upstream to spec:
458421af46
Update OSC 5522 writes to reject every transaction that exceeds the
configured decoded-data limit. The previous behavior truncated text
while rejecting only non-text data.
Programs now receive EFBIG as soon as a write crosses the limit. The
clipboard remains untouched, and remaining write packets are ignored
until a new transaction begins. Raise the default to the protocol
minimum of 64 MiB.
This applies the latest spec change:
32ea104192
Add a new `clipboard-write-limit-bytes` option (similar to
`scrollback-limit-bytes`) to limit the maximum OSC 5522 write size.
Defaults to 32 MB.
This also adds a new `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE_MAX_BYTES`
option for libghostty-vt embedders to control the same.
Kitty has a limit too and it works by truncating all data. I decided on
purpose to diverge from this because I don't think truncated binary data
is useful. Instead, we reject it so the application knows the write
didn't work.
We truncate text data, and we try to do it at the nearest complete UTF-8
sequence (if possible).
For the future: Kitty spools any write data more than some size (can't
remember) to a temp file on disk. We might want to consider doing
something similar since we're all in-memory at the moment. This PR
doesn't change that.
A Kitty clipboard protocol (OSC 5522) read that only requests the
targets type ('.') is now served without a permission prompt and never
consults (or consumes) session password grants.
The spec requires this so that a client listing the available data types
before reading one doesn't present the user with a double permission
prompt.
Add a new `clipboard-write-limit-bytes` option (similar to
`scrollback-limit-bytes`) to limit the maximum OSC 5522 write size.
Defaults to 32 MB.
This also adds a new `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE_MAX_BYTES`
option for libghostty-vt embedders to control the same.
Kitty has a limit too and it works by truncating all data. I decided on
purpose to diverge from this because I don't think truncated binary data
is useful. Instead, we reject it so the application knows the write
didn't work.
We truncate text data, and we try to do it at the nearest complete UTF-8
sequence (if possible).
Discussion #13979
Dropped paths and text once again honor bracketed paste mode. IME,
dictation, emoji picker, and character viewer commits remain typed
input.
sendText calls ghostty_surface_text, which applies the clipboard paste
pipeline and bracketed paste framing when enabled. Separating the paths
at the drag-and-drop caller preserves the input-method behavior
introduced by #13817.
A Kitty clipboard protocol (OSC 5522) write transaction targeting
`loc=primary` replied `type=write:status=DONE` in the macOS app even
though macOS has no primary selection and the data was silently
discarded.
The spec requires ENOSYS when the requested location is not available on
the system, which the read path already answers correctly:
https://sw.kovidgoyal.net/kitty/clipboard/
A Kitty clipboard protocol (OSC 5522) read that only requests the
targets type ('.') is now served without a permission prompt and never
consults (or consumes) session password grants.
The spec requires this so that a client listing the available data types
before reading one doesn't present the user with a double permission prompt.
A Kitty clipboard protocol (OSC 5522) write transaction targeting
`loc=primary` replied `type=write:status=DONE` in the macOS app even
though macOS has no primary selection and the data was silently
discarded.
The spec requires ENOSYS when the requested location is not
available on the system, which the read path already answers correctly:
https://sw.kovidgoyal.net/kitty/clipboard/
Discussion #13979
Dropped paths and text once again honor bracketed paste mode.
IME, dictation, emoji picker, and character viewer commits remain typed input.
sendText calls ghostty_surface_text, which applies the clipboard paste
pipeline and bracketed paste framing when enabled. Separating the
paths at the drag-and-drop caller preserves the input-method behavior
introduced by #13817.
Programs can now write the system clipboard through the Kitty clipboard
protocol in the macOS app. This also does all the hard work plumbing
through core termio/apprt so GTK should be an easy follow.
This functionality lets clients copy arbitrary representations (images,
HTML, etc.) into the clipboard. Writes honor `clipboard-write`: allow
applies silently, deny answers EPERM up front before any data is used,
and ask shows the standard confirmation prompt.
After this, I believe the core and macOS have 100% Kitty clipboard
implementation but I'll double check after this.
## Demo
https://github.com/user-attachments/assets/71234fa0-f539-48eb-a633-8dea3addddd5
Programs can now write the system clipboard through the Kitty
clipboard protocol in the macOS app. This also does all the hard work
plumbing through core termio/apprt so GTK should be an easy follow.
This functionality lets clients copy arbitrary representations (images,
HTML, etc.) into the clipboard. Writes honor `clipboard-write`: allow
applies silently, deny answers EPERM up front before any data is used,
and ask shows the standard confirmation prompt.
Clearing the screen into scrollback and then printing could crash debug
builds with a page integrity violation, or silently corrupt
style/hyperlink reference counts in release builds. Found in #13991 via
fuzzing.
The cursor's style and hyperlink IDs are only valid on the page the
cursor is on. When the scroll clear moved the start of the fresh screen
onto a new page, the reset path in cursorReload updated the cursor's
position directly instead of going through cursorChangePin, so the
cursor kept IDs from its old page. On the new page those IDs pointed at
entries that were dead or belonged to something else, and the next print
used them.
Fix this by making the reset path go through `cursorChangePin` like
every other cross-page cursor move, which releases the style and
hyperlink from the old page and recreates them on the new one.
Clearing the screen into scrollback and then printing could crash debug
builds with a page integrity violation, or silently corrupt style/hyperlink
reference counts in release builds. Found in #13991 via fuzzing.
The cursor's style and hyperlink IDs are only valid on the page the
cursor is on. When the scroll clear moved the start of the fresh
screen onto a new page, the reset path in cursorReload updated the
cursor's position directly instead of going through cursorChangePin,
so the cursor kept IDs from its old page. On the new page those IDs
pointed at entries that were dead or belonged to something else, and
the next print used them.
Fix this by making the reset path go through `cursorChangePin` like
every other cross-page cursor move, which releases the style and
hyperlink from the old page and recreates them on the new one.
Profiling `mpv --vo=kitty --vo-kitty-use-shm <video>` shows up a copy
and then swizzle in `prepImage`
This comes from the renderer copying the raw image data for ownership,
then doing an rgb to rgba conversion to replace the copied data.
This change optimize this case by letting the format conversion read
from the data source instead of a copy of it.
<img width="3825" height="1579" alt="image"
src="https://github.com/user-attachments/assets/25851c04-b582-4bad-8f8d-930448d89fa2"
/>
<img width="3825" height="1579" alt="image"
src="https://github.com/user-attachments/assets/fbb1ca73-a86f-421b-992d-a764ce08c83e"
/>
> Above-Before: prepImage profiles a memcpy + swizzle
> Below-After: prepImage profiles just a swizzle
The existing data path for kitty images is:
```
Read tty for Kitty image transmission (srgb, srgba, or PNG bytes)
-> copy into Kitty graphics ImageStorage (cpu-owned)
Renderer updateFrame creates Image.Pending in renderer-owned CPU storage
-> sync Kitty ImageStorage with renderer-owned ImageMap
-> copy bytes into renderer.ImageMap (renderer-owned)
-> convert ImageMap bytes to preferred GPU upload format
Renderer drawFrame uploads image data to the GPU
-> iterate through renderer.ImageMap
-> for Pending image uploads
-> convert the pixel format (no-op if already done), create the GPU-side texture and upload the data
```
# Note
Kitty graphics only supports RGB and RGBA data
The image file decode path uses a wuffs png and jpeg decode function
configured to return RGBA 8-bit, so I think in practice we only ever
upload RGB8 or RGBA8. And the gray-alpha and gray pixel formats aren't
ever used.
# AI Disclosure
I didn't use any LLM assistance for this.
This adds Kitty clipboard protocol _read_ support to macOS. In the
process, this also does most of the core termio, apprt, and Surface work
so GTK is likely very easy to do, I just didn't have the machine on hand
to test at the given moment. I will create an issue to follow up with
that.
This fully supports:
- Non-text data, like images! For this, we show an image preview.
- Per-program "remember"
- Showing the program name if given instead of generic "An application"
<img width="1848" height="996" alt="CleanShot 2026-08-24 at 08 37 45@2x"
src="https://github.com/user-attachments/assets/549d9031-2e98-46bf-90d4-94171b255c42"
/>
The `clipboard_write` effect now is similar to read: it must response to
a "reply" callback synchronously. This lets the embedder ask for write
permission, too.
We also now pass through program name and grant information from Kitty
clipboard protocol so that embedders can use that if they want.
This is a breaking ABI change.
The `clipboard_write` effect now is similar to read: it must response
to a "reply" callback synchronously. This lets the embedder ask for write
permission, too.
We also now pass through program name and grant information from Kitty
clipboard protocol so that embedders can use that if they want.
This is a breaking ABI change.
Report Kitty paste event mode 5522 as unrecognized when the stream
handler has no clipboard_read effect.
Previously libghostty-vt advertised Kitty paste events unconditionally
but Kitty clipboard reads can't work without a clipboard read effect
set.
Report Kitty paste event mode 5522 as unrecognized when the stream
handler has no clipboard_read effect.
Previously libghostty-vt advertised Kitty paste events
unconditionally but Kitty clipboard reads can't work without a clipboard
read effect set.
Translate printable physical keybindings through the current macOS
keyboard layout before assigning menu key equivalents. Previously these
bindings could not be represented because SwiftUI shortcuts are
character-based, so actions such as `super+backquote` had no native menu
shortcut.
Keep native keycodes as dispatch identity so translated display
characters do not change physical semantics or precedence over Unicode
bindings. Refresh shortcuts when the input source changes, and prevent
AppKit from transforming equivalents that are already localized.
**AI Usage:** The approach was suggested by GPT 5.6 Sol, but I wrote
most of the code and understand it all.
Follow up to #13978
`ghostty_terminal_paste` no longer takes the clipboard's data up front.
The request now carries only the list of available MIME types plus a a
callback that writes one representation's bytes into a `GhosttyWriter`.
Previously an embedder had to load every representation for every MIME
type into memory before pasting. For a clipboard holding a large image
or video next to some text that could be hundreds of megabytes that were
never used.
I also took care to make sure that the data is only read once, to avoid
any time-of-check/time-of-use (TOCTOU) issues.
There is only one case where data might be fully buffered in memory now:
unsafe text data that needs to be checked. This is true for how Ghostty
GUI works today too.
Follow up to #13978
`ghostty_terminal_paste` no longer takes the clipboard's data up front.
The request now carries only the list of available MIME types plus a
a callback that writes one representation's bytes into a `GhosttyWriter`.
Previously an embedder had to load every representation for every MIME
type into memory before pasting. For a clipboard holding a large image
or video next to some text that could be hundreds of megabytes that
were never used.
I also took care to make sure that the data is only read once, to avoid
any time-of-check/time-of-use (TOCTOU) issues.
There is only one case where data might be fully buffered in memory now:
unsafe text data that needs to be checked. This is true for how Ghostty
GUI works today too.
**Note: this has no changes for Ghostty GUI yet.** This only impacts
libghostty-vt.
This introduces a new `ghostty_terminal_paste` C API along with a
central `terminal.paste.paste` function that handles (1) mode 5522
(Kitty clipboard) (2) bracketed paste (3) normal paste all in one place,
combined with unsafe value detection and proper xterm-style newline
handling.
Terminal pasting is now stateful because for the Kitty clipboard
protocol in particular, it must mint "grants" that stay with the
terminal. Previously, paste encoding was stateless.
To start, this is only exposed/used by libghostty to enable Kitty
clipboard handling.
Other changes:
- **IO: randomSecure.** This also adds the `io.randomSecure`
implementation to `TinyIo` and a global sys override for it because
Kitty clipboard requires the ability to create one-time passwords and
the implementation (following Kitty) requires a crypto random source.
The sys model is for libghostty embedders.
- **New C result value: rejected.** This introduces a new C result enum
value "rejected" for values that are valid but rejected for some reason.
Its very possible that prior "invalid value" users will have to update
to this, and I recognize that its close to both but it fills an
important semantic difference.
Also note this still _eagerly_ requires all clipboard contents. I want
to move to a callback based model but it made the PR much more
complicated. I plan on playing with that before converting apprt's to
this.