The pipelined POSIX pty reader uses multiple large gather buffers to avoid
stalling on Darwin, where pty reads are capped around 1KiB. On Linux this can
let bulk terminal producers run too far ahead of terminal parsing/rendering.
Frame-style terminal apps such as DOOM-fire can then report very high producer
FPS while Ghostty displays stale frames from the buffered stream.
Keep the Darwin-tuned 4 x 64KiB pipeline, but reduce non-Darwin read-ahead to
2 x 8KiB so the pty can apply backpressure before multiple frames are queued.
#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 replaces the single-threaded pty read loop on posix systems with
a two-stage pipeline: a new `io-gather` thread drains the pty into a
small ring of large buffers while the existing `io-reader` thread
parses the previous batch concurrently.
The motivating discovery was actually found by Fable, but the
resulting code was hand-written and hand-verified (in addition to
model-verified as an extra check): on macOS the kernel tty output queue
caps every read on the pty master at about 1 KiB regardless of the
read buffer size. Instrumenting a pty with a 64 KB buffer while
streaming a 6.49 MB file produced 6,337 reads where every read was exactly
1024 bytes.
This immediately made me realize two things about the old loop that we've
had since like 2023 which called processOutput once per read: all per-call
overhead (terminal lock, render wakeup, cursor timestamp) was paid per
kilobyte of bulk output, and the child could never run more than 1 KiB
ahead of us, so while we parsed the child (e.g. `cat`) sat blocked on a full
kernel queue instead of producing.
In 2023, I justified this architecture by saying "reads are generally small"
but I didn't understand then that reads are generally small because the
kernel makes them small even if there is a lot of data.
To preserve latency for the more typical
small-reads-that-are-actually-small, sub-1 KB payloads deliver on the first
EAGAIN with no added latency.
End-to-end throughput was measured by timing `cat file > /dev/ttysN`
against a fresh app instance (M4 Max, macOS 26, ReleaseFast, medians
of repeated interleaved A/B runs):
| stream | before | after | change |
|-------------------------|--------------|---------------|---------|
| ascii.txt (6.5 MB) | 91-92 MB/s | 114-123 MB/s | +25-30% |
| unicode.txt (8 MB) | 116-117 MB/s | 180-183 MB/s | +55% |
| DOOM-Fire-Zig | 530 fps | 770 fps | +45% |
The pipeline now delivers the parse stage's full measured capacity
(the parse thread is pegged while gather spends ~33% of a core, so
any IO throughput improvements are now fully parser-bound).
**Linux note:** This needs to be verified on Linux. I think broadly
architecture is better and should never be worse. But its possible
some of the magic constants need to be tuned differently. Would love
more testing there.
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.
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`.
Per review feedback, cover the four Windows branches added in the
parent commit:
- bare `cmd.exe` resolves via `%COMSPEC%` (with documented fallback)
- bare non-cmd shell (`pwsh.exe`) is passed through unchanged
- shell value with arguments (`wsl ~`) is split on whitespace
- direct command is passed through without modification
Co-authored-by: Claude <noreply@anthropic.com>
On Windows the shell value was always executed as `cmd.exe /C <shell>`.
For even a simple `command = wsl ~` this spawned two processes (the
cmd wrapper and the user's actual shell) and had visible side effects:
an extra cmd.exe in the process tree, and cmd AutoRun state (DOSKEY
aliases, `cd` in init.cmd, etc.) running in the wrapper rather than
the user's shell, since AutoRun is per-process.
Run the shell value directly. If it contains whitespace, split on
whitespace into argv. Bare `cmd.exe` is resolved via %COMSPEC% which
is the documented path to the current command processor; other bare
values are left to PATH resolution in Command.startWindows.
The simple whitespace split does not honor Windows CLI quoting rules.
Users who need quoted arguments should use the direct command form.
Also skip the termios focus timer on Windows since Windows has no
termios; the focusGained callback was starting a timer whose callback
would then do nothing.
Co-authored-by: Claude <noreply@anthropic.com>
Move kitty_image_storage_limit and kitty_image_loading_limits into
Terminal.Options so callers can set them at construction time
rather than calling setter functions after init. The values flow
through to Screen.Options during ScreenSet initialization. Termio
now passes both at construction, keeping the setter functions for
the updateConfig path.
Add a Limits type to LoadingImage that controls which transmission
mediums (file, temporary_file, shared_memory) are allowed when
loading images. This defaults to "direct" (most restrictive) on
ImageStorage and is set to "all" by Termio, allowing apprt
embedders like libghostty to restrict medium types for resource or
security reasons.
The limits are stored on ImageStorage, plumbed through
Screen.Options for screen initialization and inheritance, and
enforced in graphics_exec during both query and transmit. Two new
Terminal methods (setKittyGraphicsSizeLimit, setKittyGraphicsLoadingLimits)
centralize updating all screens, replacing the manual iteration
previously done in Termio.
Fixes#11957
erasePage now updates page_serial_min when the first page is erased,
and asserts that only front or back pages are erased since
page_serial_min cannot represent serial gaps from middle erasure.
To enforce this invariant at the API level, PageList.eraseRows is
now private. Two public wrappers replace it: eraseHistory always
starts from the beginning of history, and eraseActive takes a y
coordinate (with bounds assertion) and always starts from the top
of the active area. This makes middle-page erasure impossible by
construction.
Renames `ReadonlyStream` to `TerminalStream` and introduces an
effects-based callback system so that the stream handler can optionally
respond to queries and side effects (bell, title changes, device
attributes, device status, size reports, XTVERSION, ENQ, DECRQM, kitty
keyboard queries).
The default behavior is still read-only, callers have to opt-in to
setting callbacks to function pointers.
This doesn't handle every possible side effect yet, e.g. this doesn't
include clipboards, pwd reporting, and others. But this covers the
important ones.
This PR is Zig only, the C version of this will come later.
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.
Make the "cache directory paths" test cross-platform by using
std.fs.path.join for expected values and a platform-appropriate
mock home path, since the function under test uses native path
separators.
Skip the two shell integration XDG_DATA_DIRS tests on Windows.
These tests use hardcoded Unix path separators (:) and Unix default
paths (/usr/local/share:/usr/share) which are not applicable on
Windows where the path delimiter is ; and XDG_DATA_DIRS is not a
standard concept.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This adds a function to the core surface to get process information
about the process(es) running in the terminal. Currently supported is
the PID of the foreground process and the name of the slave PTY.
If there is an error retrieving the information, or the platform does
not support retieving that information `null` is returned.
This will be useful in exposing the foreground PID and slave PTY name to
AppleScript or other APIs.
Size report escape sequences were previously formatted inline in
Termio.sizeReportLocked, and termio.Message carried a duplicate enum for
report styles. That made the encoding logic harder to reuse and kept
the style type scoped to termio.
Move the encoding into terminal.size_report and export it through
terminal.main. The encoder now takes renderer.Size directly and derives
grid and pixel dimensions from one source of truth. termio.Message now
aliases terminal.size_report.Style, and Termio writes reports via the
shared encoder.
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).
Add focus event encoding (CSI I / CSI O) to the libghostty-vt public
API, following the same patterns as key and mouse encoding.
The focus Event enum uses lib.Enum for C ABI compatibility. The C API
provides ghostty_focus_encode() which writes into a caller-provided
buffer and returns GHOSTTY_OUT_OF_SPACE with the required size when
the buffer is too small.
Also update key and mouse encoders to return GHOSTTY_OUT_OF_SPACE
instead of GHOSTTY_OUT_OF_MEMORY for buffer-too-small errors,
reserving OUT_OF_MEMORY for actual allocation failures. Update all
corresponding header documentation.
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.
From #10554
> I can see there being space for some kind of new sequence that tells
the terminal that "hey, I want a semantic palette" or something, that is
better adjusted to themes automatically. But, I don't think we should
this by default.
> So my concrete proposal is to eliminate the inversion and bg => fg
ramp and do a more typical dark => light ramp (still taking into account
bg/fg, but keeping 16 black and 231 white). I think this is the only way
we can really make this a default on feature. I think this would address
all the negative reactions we've gotten to it.
This adds a new `palette-harmonious`, disabled by default, which allows
generated light themes to be inverted.
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 the way Ghostty assigns itself and subprocesses to
cgroups and how resource controls are applied.
* Ghostty itself no longer modifies it's own cgroup or moves itself
to a transient scope. To modify the main Ghostty process' resource
controls ensure that you're launching Ghostty with a systemd unit and
use the standard systemd methods for overriding and applying changes
to systemd units.
* If configured (on by default), the process used to run your command
will be moved to a transient systemd scope after it is forked from
Ghostty but before the user's command is executed. Resource controls
will be applied to the transient scope at this time. Changes to
the `linux-cgroup*` configuration entries will not alter existing
commands. If changes are made to the `linux-cgroup*` configuration
entries commands will need to be relaunched. Resource limits can also
be modified after launch outside of Ghostty using systemd tooling. The
transient scope name can be shown by running `systemctl --user whoami`
in a shell running inside Ghostty.
Fixes#2084.
Related to #6669
Example of `systemctl status` showing main Ghostty process and one
surface:
<img width="1132" height="135" alt="Screenshot From 2026-02-07 16-31-14"
src="https://github.com/user-attachments/assets/81dffd0b-8801-4695-adf4-213647cdf0c3"
/>
This fixes the issue where our palette generation was changing our
default palette. The default palette is based on some well known values
chosen from various terminals and it was a bit jarring to have it
change.
We now only auto-generate the palette if the user has customized at
least one entry.
This changes the way Ghostty assigns itself and subprocesses to
cgroups and how resource controls are applied.
* Ghostty itself no longer modifies it's own cgroup or moves itself
to a transient scope. To modify the main Ghostty process' resource
controls ensure that you're launching Ghostty with a systemd unit and
use the standard systemd methods for overriding and applying changes
to systemd units.
* If configured (on by default), the process used to run your command
will be moved to a transient systemd scope after it is forked from
Ghostty but before the user's command is executed. Resource controls
will be applied to the transient scope at this time. Changes to
the `linux-cgroup*` configuration entries will not alter existing
commands. If changes are made to the `linux-cgroup*` configuration
entries commands will need to be relaunched. Resource limits can also
be modified after launch outside of Ghostty using systemd tooling. The
transient scope name can be shown by running `systemctl --user whoami`
in a shell running inside Ghostty.
Fixes#2084.
Related to #6669
The `cursor` shell feature always used a blinking bar (beam), often to
the surprise of users who set `cursor-style-blink = false`.
This change extends our GHOSTTY_SHELL_FEATURES format to include either
`cursor:blink` (default) or `cursor:steady` based on cursor-style-blink
when the `cursor` feature is enabled, and all shell integrations have
been updated to use that additional information to choose the DECSCUSR
cursor value (5=blinking bar, 6=steady bar).
I also considered passing a DECSCUSR value in GHOSTTY_SHELL_FEATURES
(e.g. `cursor:5`). This mostly worked well, but zsh also needs the blink
state on its own for its block cursor. We also don't support any other
shell feature cursor configurability (e.g. the shape), so this was an
over generalization.
This does change the behavior for users who like the blinking bar in the
shell but have `cursor-blink-style = false` for other reasons. We could
provide additional `cursor` shell feature configurability, but I think
that's best left to a separate change.
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
Nushell <https://www.nushell.sh/> is a modern interactive shell that
provides many shell features out-of-the-box, like `title` support. Our
shell integration therefore focuses on Ghostty-specific features like
`sudo`.
We use Nushell's module system to provide a `ghostty` module containing
our shell integration features. This module is automatically loaded from
$XDG_DATA_DIRS/nushell/vendor/autoload/ when `nushell` shell integration
is enabled.
Exported module functions need to be explicitly "used" before they're
available to the interactive shell environment. We do that automatically
by adding `--execute "use ghostty *"` to the `nu` command line.
This imports all available functions, and individual shell features are
runtime-guarded by the script code (using $GHOSTTY_SHELL_FEATURES). We
can consider further refining this later.
When automatic shell integration is disabled, users can still manually
source and enable the shell integration module:
source
$GHOSTTY_RESOURCES_DIR/shell-integration/nushell/vendor/autoload/ghostty.nu
use ghostty *
This initial work implements our TERMINFO-aware `sudo` wrapper (via the
`sudo` shell feature). Support for additional features, like `ssh-env`
and `ssh-terminfo`, will follow (#9604).
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
This makes our 'ghostty' module available even if the rest of our
automatic integration steps fail, which is convenient for manual
"use"-age.
This is safe because autoload-ing our module doesn't have any side
effects other than cleaning up the XDG_DATA_DIRS environment variable.