The libghostty-vt stream is made to be infallible: in the case of any error
it just logs and moves on. That's because a terminal can't really... stop,
under normal operations. But, under special operations (fuzzing, replays,
etc.) it can and should stop!
Rather than make the operation fallible, its simply enough for me at least
to know that something went wrong. This is a simple change that adds a simple
flag that is flagged to true when such a scenario happens.
For normal Ghostty GUI operations, this isn't used at all. For libghostty
consumers they can choose to read it if they want, but don't have to.
This also adds a C API to read it.
Terminal resize could leave tab stops, pixel geometry, synchronized output,
or screen dimensions partially updated when a later allocation failed. This
is now safe. There is one exception, see the code comments.
This makes the Terminal.resize handle more of the common elements that
a core terminal emulator should: cell geoemtry handling (if exists),
updates synchronized output modes.
This adds a new TerminalStream.resize that also handles the side effects
for more easy integration into downstream libghostty-vt consumers, namely
mode 2048 in-band signaling handling.
libghostty-vt was already parsing OSC 52 into the clipboard_contents
action but the stream handler dropped, so there was no way to observe
clipboard writes (in this case, a program using go-libghostty). This
adds a clipboard_set effect following the existing bell/title_changed
pattern and expose it through the C API as
`GHOSTTY_TERMINAL_OPT_CLIPBOARD_SET`.
The callback receives the OSC 52 kind byte and the base64 payload
exactly as received; decoding and kind interpretation are left to the
embedder, matching how decoding is typically deferred.
Note this intentionally does not deal with clipboard read requests given
the security implications.
AI disclosure: Fable (via Claude Code) did the majority of the work
here, I validated it on the client side and fully understand the pattern
we're fitting into.
#13182
Replace the OSC 52-specific kind and encoded payload callback with an
atomic clipboard write containing a normalized destination and decoded
MIME representations. This keeps protocol details out of embedders and
lets iTerm2 Copy use the same semantic path.
Represent clears with an empty content list, preserve binary payloads,
and return a generic result for protocols that acknowledge writes. Add
the C ABI descriptors, layout metadata, and effects example so future
multipart protocols can reuse the callback without another API break.
APC payloads such as Kitty graphics images can be megabytes of base64
data, but every byte was dispatched individually: through the VT state
machine table, an apc_put action, the stream handler, the APC protocol
handler, and finally a per-byte ArrayList append in the Kitty command
parser. Five layers of dispatch per byte made large image transfers
far slower than they needed to be.
Add a bulk fast path alongside the existing CSI fast paths in
consumeUntilGround: scan the longest run of apc_put bytes (stopping
at any byte the parse table doesn't treat as APC payload: CAN, SUB,
ESC, and most C1 bytes exit or abort the string state, and 0xA0-0xFF
are ignored by it) and dispatch the run as a single new apc_put_slice
action. The APC handler identifies the protocol from the first few
bytes as before, then passes the remainder of each slice to the
protocol parser in bulk; the Kitty parser appends payload data with a
single appendSlice. Ignored/unknown APC sequences now drop each slice
in O(1) instead of per-byte dispatch.
The fast path is guarded the same way as the CSI fast paths: handlers
with a vtRaw hook (the inspector) keep receiving per-byte apc_put
actions, and the scalar next() path is unchanged.
Also add benchmark support: a `ghostty-gen +kitty` synthetic generator
emitting well-formed Kitty graphics transmit commands with 4 KiB
random base64 payloads (not valid image data; the corpus exercises
the parsing paths, not image decoding), and a `ghostty-bench
+apc-parser` benchmark that measures the stream -> APC -> Kitty parse
path without image decode/storage.
Benchmarks on a 64 MiB corpus (hyperfine, ReleaseFast, x86_64 Linux,
baseline is identical source with only the fast path disabled):
apc-parser: 1.061 s -> 43 ms (~25x)
terminal-stream (kitty): 1.163 s -> 72 ms (~16x)
terminal-stream (ascii): no change
The ascii case was verified with retired instruction counts (perf
stat, pinned to one core) since wall time on the test machine has
4-7 ms of noise: 988,030,458 vs 988,045,833 instructions (+0.0016%),
a fixed startup-size delta; the ground-state hot loop never reaches
the new branch.
libghostty-vt already tracked OSC color state but ignored color queries in the standalone stream handler. This meant embedders that installed write_pty still received no response for OSC 4/10/11/12 or Kitty OSC 21 queries.
Resolve the current terminal colors through shared Terminal helpers and encode replies through the write_pty effect. Xterm queries use the fixed 16-bit rgb form, preserve the request terminator, and fall back from cursor to foreground when no cursor color is set. Kitty color queries now report supported terminal-backed keys and return empty values for unset dynamic colors.
Add RGB wire encoders and tests covering the stream handler and C API. The OSC parser now releases color operation request lists during reset, fixing an allocation leak exposed by multi-query OSC color tests.
#13209
After #13209 the IO pipeline delivers the parse thread's full
measured capacity, so IO throughput is now bound by VT processing.
Profiling `terminal-stream` on plain text showed ~85% of wall time
inside Terminal.print: every printable codepoint paid the full
per-character cost (right margin computation, grapheme clustering
checks, width lookup, wrap/insert mode checks, charset mapping,
per-cell style bookkeeping, dirty marking, cursor advance) even
though for typical bulk output every one of those answers is the
same for thousands of consecutive characters.
This adds a new print_slice stream action carrying a run of
printable codepoints, emitted whenever the SIMD ground-state path
decodes multiple codepoints at once, plus Terminal.printSlice which
processes such runs in batch. Since action dispatch is comptime,
delivering a slice through the existing vt handler interface has
the same codegen as a dedicated entry point; handlers that don't
care about batching can simply loop and treat each codepoint as a
print action.
printSlice hoists all run-invariant checks (status display, insert
and wraparound modes, charset state, hyperlink state) out of the
loop and then fills cells row by row. A single masked u64 compare
classifies each destination cell as "simple" (plain codepoint cell,
narrow, no hyperlink, style already matching the cursor); runs of
simple cells are written with a branch-free store loop, style-only
mismatches are handled inline with the same ref-counting printCell
does, and anything needing real cleanup (wide spacers, grapheme
data, hyperlinks) exits the fast path with the cursor positioned on
the offending cell so print() handles that one codepoint with full
generality. Dirty marking, previous_char, and cursor advancement
happen once per row instead of once per character.
The fast path handles both narrow and wide codepoints (CJK/emoji are
written as wide+spacer_tail pair fills, including spacer-head
handling at the right edge) and stays exact under grapheme
clustering (mode 2027): a codepoint only joins a run if it is width
1 or 2 and is a grapheme break from the previously written
codepoint, so print() would never have attached it to the previous
cell. The first codepoint of a batch defers to print() whenever the
previous cell could carry cluster state we can't cheaply reason
about (including a pending wrap, where print attaches to the
pending cell instead of wrapping).
Correctness is verified by a new differential fuzz test that runs
the same operations through per-codepoint print and randomly
chunked printSlice, comparing full screen dumps, cursor state, and
page integrity (style refcounts, grapheme maps) after every
operation, across wraps, margins, mode toggles, hyperlinks,
charsets, and wide/combining/ZWJ/RI/jamo codepoints.
Throughput measured with ghostty-bench terminal-stream (full
terminal handler, 100 MB deterministic corpora, 120x80, M4 Max,
ReleaseFast, hyperfine means of 10 runs; ~15ms process startup
included in all numbers):
| stream | before | after | change |
|---------------------------|--------|--------|--------|
| ascii (no newlines) | 784 ms | 138 ms | 5.7x |
| ascii lines | 833 ms | 198 ms | 4.2x |
| unicode mixed-script | 779 ms | 320 ms | 2.4x |
| CJK (all wide) | 424 ms | 126 ms | 3.4x |
| unicode, mode 2027 on | 807 ms | 367 ms | 2.2x |
| CJK, mode 2027 on | 495 ms | 198 ms | 2.5x |
Add a shared encoder for CSI ? 997 ; Ps n color scheme reports and use
it for both CSI ? 996 n replies and unsolicited Termio reports. Export the
same encoder through the libghostty-vt C API with docs and an example.
This is a really light API, arguably easy for consumers to hardcode,
but it didn't match the rest of our style in the libghostty API so we
should expose it.
Example: GHOSTTY_COLOR_SCHEME_DARK encodes to ESC [ ? 997 ; 1 n,
while GHOSTTY_COLOR_SCHEME_LIGHT encodes to ESC [ ? 997 ; 2 n.
libghostty-vt already parses OSC 52 into the clipboard_contents action but
the stream handler dropped it in the no-effect list, so embedders had no way
to observe a program's clipboard writes. Add a clipboard_set effect following
the existing bell/title_changed pattern and expose it through the C API as
GHOSTTY_TERMINAL_OPT_CLIPBOARD_SET.
The callback receives the OSC 52 kind byte and the base64 payload exactly as
received; decoding and kind interpretation are left to the embedder, matching
how ghostty itself defers decoding to the apprt layer.
Clipboard read requests ("?") are never forwarded: answering one would let
any program running in the terminal silently read the user's clipboard, and
a VT state library cannot mediate that with user consent. Empty payloads are
also ignored rather than inventing clear semantics.
Previously the libghostty-vt stream handler dropped .report_pwd as a
no-op, so embedders never saw shell-reported cwd changes and the
terminal's pwd field was never populated from escape sequences.
Wire the action to setPwd and expose a pwd_changed callback analogous
to title_changed via GHOSTTY_TERMINAL_OPT_PWD_CHANGED. The payload is
passed through unparsed; embedders read it with ghostty_terminal_get
and decode any URI scheme themselves.
This hooks up the glyph protocol glossary to the terminal state. This
effectively makes us handle the APC protocol for it both in Ghostty GUI
and libghostty, although we didn't implement the renderer yet.
The Zig/C libghostty API also has a way to disable the protocol but it is
enabled by default. The memory usage is bound by the specification.
For dirty tracking for the renderer, we're going with the simple route that
any glyph change marks a coarse grained dirty flag and we'll [in the future]
rebuild the entire state in the renderer. I think this will be fine for
realistic workloads, but we can reassess in the future when we have
real workloads.
Adds an option to `libghostty-vt` to configure the default cursor style
that should be displayed when an app sends a DECSCUSR reset sequence
(`CSI 0 q`).
This adds the core parse/encode for the still in-development and experimental
terminal glyph protocol: https://github.com/raphamorim/rio/pull/1542
Up to version 1.9.
The only cross-cutting change necessary was changing the APC
identification logic which previously only looked at a single byte to
support multi-byte identifiers since the glyph protocol uses `25a1`.
Wire up the APC handler to `terminal.TerminalStream` to process
APC sequences, enabling support for kitty graphics commands in
libghostty, in theory.
The "in theory" is because we still don't export a way to actually
enable Kitty graphics in libghostty because we have some other things in
the way: PNG decoding and OS filesystem access that need to be more
conditionally compiled before we can enable the feature. However, this
is a step in the right direction, and we can at least verify that the
APC handler works via a test in Ghostty GUI.
Add a device_attributes effect callback to the stream_terminal
Handler. The callback returns a device_attributes.Attributes
struct which the handler encodes and writes back to the pty.
Add Attributes.encode which dispatches to the correct sub-type
encoder based on the request type (primary, secondary, tertiary).
In readonly mode the callback is null so all DA queries are
silently ignored, matching the previous behavior where
device_attributes was in the ignored actions list.
Tests cover all three DA types with default attributes, custom
attributes, and readonly mode.
Previously device_status was in the ignored "no terminal-modifying
effect" group in stream_terminal.zig. This ports it to use the
Effects pattern, handling all three DSR request types.
Operating status and cursor position are handled entirely within
stream_terminal since they only need terminal state and write_pty.
Cursor position respects origin mode and scrolling region offsets.
Color scheme adds a new color_scheme effect callback that returns
a ColorScheme enum (light/dark). The handler encodes the response
internally, keeping protocol knowledge in the terminal layer. A
new ColorScheme type is added to device_status.zig so the terminal
layer does not depend on apprt.
Previously the ENQ (0x05) action was ignored in stream_terminal,
listed in the no-op group alongside other unhandled queries. The
real implementation in termio/stream_handler writes a configurable
response string back to the pty.
Add an enquiry callback to Effects following the same query-style
pattern as xtversion: the callback returns the raw response bytes
and the handler owns writing them to the pty via writePty. When no
callback is set (readonly mode), ENQ is silently ignored. Empty
responses are also ignored. The response is capped at 256 bytes
using a stack buffer with sentinel conversion for writePty.
Add a `size` callback to the stream_terminal Effects struct that
returns a size_report.Size geometry snapshot for XTWINOPS size
queries (CSI 14/16/18 t). The handler owns all protocol encoding
using the existing size_report.encode, keeping VT knowledge out
of effect consumers. This follows the same pattern as the xtversion
effect: the callback supplies data, the handler formats the reply
and calls write_pty.
CSI 21 t (title report) is handled internally from terminal state
since the title is already available via terminal.getTitle() and
does not require an external callback.
Add an xtversion callback to the Effects struct so that
stream_terminal can respond to XTVERSION queries. The callback
returns the version string to embed in the DCS response. If the
callback is unset or returns an empty string, the response defaults
to "libghostty". The response is formatted and written back via the
existing write_pty effect.
Previously kitty_keyboard_query was listed as a no-op in the
readonly stream handler. This implements it using the write_pty
effect callback so that the current kitty keyboard flags are
reported back via the pty, matching the behavior in the full
stream handler.
The effect callback no longer receives the title string directly.
Instead, the handler stores the title in terminal state via setTitle
before invoking the callback, so consumers query it through
handler.terminal.getTitle(). This removes the redundant parameter
and keeps the effect signature consistent with the new terminal
title field. Tests now verify terminal state directly rather than
tracking the title through the callback.
Previously the window_title action was silently ignored in the
readonly stream handler. Add a set_window_title callback to the
Effects struct so callers can be notified when a window title is
set via OSC 2. Follows the same pattern as bell and write_pty
where the callback is optional and defaults to null in readonly
mode.
Add a generic write_pty effect callback to the stream terminal
handler, allowing callers to receive pty response data. Use it to
implement request_mode and request_mode_unknown (DECRQM), which
encode the mode state as a DECRPM response and write it back
through the callback. Previously these were silently ignored.
The write_pty data is stack-allocated and only valid for the
duration of the call.
Rename stream_readonly.zig to stream_terminal.zig and its exported
types from ReadonlyStream/ReadonlyHandler to TerminalStream. The
"readonly" name is now wrong since the handler now supports
settable effects callbacks. The new name better reflects that this
is a stream handler for updating terminal state.