Applications cannot infer whether an unfocused terminal remains visible, so
focus reports are insufficient for avoiding expensive rendering while a
view is hidden.
Implement private mode 2033 and the visibility query/report sequences.
Track conservative per-surface visibility, report every effective change
while enabled, and always answer explicit queries and mode enables. Keep
view visibility across terminal resets because it is owned by the host,
not terminal state.
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019fa965-aa5f-7099-85b4-a9679d2c8bd3
Move DECRQSS response encoding into the terminal DCS handler so both
the full termio path and libghostty-vt terminal stream emit the same
replies. The C API stream now maintains and releases DCS parser state
and forwards responses through write_pty.
This commit represents the majority of the work necessary to upgrade
Ghostty to use Zig 0.16.0.
Key parts:
* In addition to its previous responsibilities, the global state now
houses state for global I/O implementations and the process
environment. It is now also utilized in the main application along
with the C library. Where necessary, global state is isolated from key
parts of the implementation (e.g., in libghostty subsystems), and it's
expected that this list will grow.
* We currently manage our own C translation layer where necessary. In
these cases, cImport has been removed in favor of the new external
translate-c package. Due to fixes that have needed be made to properly
translate the dependencies that were swapped out, as mentioned, we
have had to backport fixes from the current translate-c package (and
the upstream Arocc dependency). We will host this ourselves until Zig
0.17.0 is released with these fixes.
* Where necessary (only a small number of cases), some stdlib code from
0.15.2 (and even from 0.17.0) has been taken, adopted, and vendored in
lib/compat.
Co-authored-by: Leah Amelia Chen <hi@pluie.me>
Cursor defaults were duplicated across stream handlers and it was a
pretty significant amount of simple and yet non-trivial logic to
understand.
Store these on Terminal itself and have methods to route things like
DECSCUSR through for consistent behaviors.
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.
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.
#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 |
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.
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`.
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.
Add a title field to Terminal, mirroring the existing pwd field.
The title is set via setTitle/getTitle and tracks the most recent
value written by OSC 0/2 sequences. The stream handler now persists
the title in terminal state in addition to forwarding it to the
surface. The field is cleared on full reset.
This extracts our mode reporting from being hardcoded in termio
to being reusable in the existing `terminal.modes` namespace. The goal
is to expose this via the Zig API libghostty (done) and C API (to do
later).
The terminal.Stream next/nextSlice functions can now no longer fail.
All prior failure modes were fully isolated in the handler `vt`
callbacks. As such, vt callbacks are now required to not return an error
and handle their own errors somehow.
Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.
There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.
This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
Implements the `output` option for the `scroll-to-bottom` configuration,
which scrolls the viewport to the bottom when new lines are printed.
Co-Authored-By: Sachin <sachinbeniwal0101@gmail.com>
This changes our OSC133 parser to parse options lazily. We do this for
multiple reasons:
1. Parsing all our options ahead of time balloons our required
osc.Command tagged union type which has C ABI implications. Adding
all supported options (including Kitty extensions) today already
breaks our C ABI.
2. Invalid options are allowed by the specification and should be
explicitly ignored, so we don't need to validate options at all
during parse time.
3. Semantic prompt markers don't need to be high throughput, so we
can afford to do some extra work at processing time to gather
the options. They're also rather short usually.
The reporting of color scheme was handled asynchronously by queuing a
handler in the surface. This could lead to race conditions where the DSR
is reported after subsequent VT sequences.
Fixes#5922
We need to have sane behavior in error handling because the running
program that sends the restore cursor command has no way to realize it
failed. So if our style fails to add (our only fail case) then we revert
to no style.
https://ampcode.com/threads/T-019bd7dc-cf0b-7439-ad2f-218b3406277a
The reporting of color scheme was handled asynchronously by queuing a
handler in the surface. This could lead to race conditions where the
DSR is reported after subsequent VT sequences.
Fixes#5922
Fixes#9905
This fixes a major compatibility issues with the CSI S sequence:
When our top margin is at the top (row 0) without left/right
margins, we should be creating scrollback. Previously, we were
only deleting.