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 is proposed in
[discussion#12927](https://github.com/ghostty-org/ghostty/discussions/12927)
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.
## Summary
- Adds a "Close Split" option to the right-click context menu in the
split submenu
- Allows users to close the focused split pane directly from the context
menu
Reference discussion:
https://github.com/ghostty-org/ghostty/discussions/10982
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 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.
Also found when test searching.
Run Ghostty debug on macOS and follow these steps:
1. Open Ghostty, `cat src/Surface.zig` and start search
`self.startClipboardRequest`.
2. Click up button(Press enter) 6 times and click down button (Press
shift+enter) 6 times.
3. You should see a panic crash.
### AI Disclosure
Claude implemented the fix and the unit test.
I reviewed it and tested it myself.
selectPrev's wrap (active_len + history_len - 1) would underflow if a
selection were live while both result lists are empty. Add a test that
exercises the invariant making that unreachable: overwriting the only match
forces a reload that empties both lists and drops the selection, so the next
select() hits the no-matches guard instead of the wrap arithmetic.
Found this issue when testing some search features; follow up for
#12907.
You can either reproduce using the PoC below with `libghostty-vt` or run
Ghostty debug on macOS and follow these steps:
1. Open Ghostty and start search `0`.
2. Press `cmd+=` to increase font size.
3. You should see a panic crash.
### AI Disclosure
As the commit suggests, Claude implemented the fix, the unit test, and
PoC file.
I reviewed it(seems reasonable to me, but I’m not a Zig professional)
and tested it myself.
```zig
// PoC: resize panic when shrinking both axes with the cursor near the top
// of a fully-populated screen.
//
// Build (with libghostty-vt headers + dylib on the standard search paths):
// zig run poc.zig -lghostty-vt
//
// Or point at a local build:
// zig run poc.zig -I <prefix>/include -L <prefix>/lib -lghostty-vt
//
// At runtime the dylib must be discoverable (DYLD_LIBRARY_PATH on macOS,
// LD_LIBRARY_PATH on Linux, or an rpath baked in at link time).
//
// Without the fix, this aborts with
// reached unreachable code (assert in PageList.Pin.pageIterator)
// at _terminal.PageList.resizeCols on a debug/safe build. On release it
// silently iterates an empty (reversed) range.
const std = @import("std");
const c = @cImport({
@cInclude("ghostty/vt.h");
});
pub fn main() !void {
var term: c.GhosttyTerminal = null;
const opts: c.GhosttyTerminalOptions = .{
.cols = 80,
.rows = 24,
.max_scrollback = 1000,
};
if (c.ghostty_terminal_new(null, &term, opts) != c.GHOSTTY_SUCCESS) {
return error.InitFailed;
}
defer c.ghostty_terminal_free(term);
// Fill every one of the 24 active rows with non-blank content. This is
// what makes the bug reachable: when rows shrink, resizeWithoutReflow
// can only trim *blank* trailing rows, so non-blank rows are instead
// pushed up into scrollback and the active-area top moves down.
{
var buf: [256]u8 = undefined;
var i: usize = 0;
while (i < 24) : (i += 1) {
// "X" on each row; CR+LF between rows but not after the last so
// we don't scroll the top row away.
const line = if (i + 1 < 24)
std.fmt.bufPrint(&buf, "X\r\n", .{}) catch unreachable
else
std.fmt.bufPrint(&buf, "X", .{}) catch unreachable;
c.ghostty_terminal_vt_write(term, line.ptr, line.len);
}
}
// CSI 1;1H -> park the cursor on the TOP row (1-based). The active area is
// anchored to the bottom, so once we shrink rows this row falls above the
// new active-area top, i.e. into scrollback.
const move = "\x1b[1;1H";
c.ghostty_terminal_vt_write(term, move.ptr, move.len);
// Shrink both axes. Columns must shrink to take resize()'s .lt branch,
// which runs the row shrink first and then resizeCols with the original
// (now out-of-active-area) cursor pin. Panics in
// _terminal.PageList.resizeCols.
_ = c.ghostty_terminal_resize(term, 79, 20, 8, 16);
std.debug.print("survived resize (fix is present)\n", .{});
}
```
This adds the glossary and request handler logic to the glyph protocol
package.
We now have a fully spec compliant business-logic part of the glyph
protocol.
**This doesn't yet hook it up to terminal state.** So it isn't impacting
any real-world usage yet.
Code was hand-written, tests were AI-assisted and human reviewed.
Extend glyph render constraints with cell-span sizing modes for height,
width, contain, cover bounds, and stretch bounds. These preserve the
existing face-targeted behavior for platform fonts, emoji, and Nerd Font
rules while giving registered glyphs a target based on terminal cell
spans.
Map Glyph Protocol registration options to the new constraint modes so
sizing follows the spec formulas based on authored advance width and line
height. Baseline alignment now places design-space y=0 on the terminal
text baseline instead of approximating it as start alignment.
Document the placement formulas in the local protocol summary and add
focused tests for constraint mapping, cell-span padding, line-height and
advance scaling, contain versus cover behavior, stretch, and baseline
placement.
Register parsing now validates the full register request shape before
constructing the parsed command. Inputs that only contain the verb
separator, such as `r`, `r;cp=e0a0`, or `r;foo`, now fail with
InvalidFormat instead of reaching Register invariants guarded by asserts.
Valid empty-payload requests still parse when they include the payload
separator, allowing execution to report malformed_payload through the
normal protocol response path.
Glyph clear execution previously treated an unparsable cp option the same
as an omitted cp option. That made inputs such as c;cp=zz behave like a
bare clear request and remove every glossary registration.
Track clear option presence separately from successful decoding. A
present but malformed cp now returns a malformed_payload clear failure
without mutating the glossary, while an omitted cp still clears all
registrations.
In the column-shrink (.lt) branch of PageList.resize, resizeWithoutReflow
lowers self.rows before resizeCols runs. Because the active area is anchored
to the bottom, shrinking rows moves the active-area top down; a cursor near
the top of the old active area then ends up above the new active area (in
scrollback).
resizeCols counts wrap continuations from the cursor pin up to the active-area
top via a .left_up rowIterator. When the cursor pin is above the limit, the
range is reversed and the iterator's order assertion fires (SIGABRT in debug;
silently iterates empty in release).
Count zero wraps when the cursor pin is above the active area, mirroring the
post-reflow preserved-cursor block which already no-ops for a non-active
cursor. Add a regression test.
This PR adds 2 options to `libghostty-vt` to configure the style and
blink status of the default cursor. They control how the terminal
renders the cursor when a program doesn't request any explicit style or
when it resets it to the terminal's default state by sending a DECSCUSR
reset sequence (`CSI 0 q`).
This adds a Glyf outline decoder and rasterizer.
So it turns out that FreeType and CoreText have very shitty APIs for raw
Glyf table rasterization. CoreText as far as I can find can't do it at
all. In both cases you have to create a synthetic font with just this
entry and rasterize the glyph. And the code to do all that was WAYYYYYY
complex such that this made way more sense.
We need this for the Glyph Protocol.
**AI disclosure:** Hand-written parser, rasterizer. AI assisted
validation and test writing. I read the spec myself.
cc @qwerasd205
The core had no signal to the apprt when the active selection changed,
so a consumer (e.g. a screen reader) kept reading a stale selection
until some unrelated query refreshed it.
This change adds a payload-less selection_changed action that's fired on
a selection state transition. The apprt reads the current selection
through the normal read path.
This consolidates selection state changes so the notification fires
consistently: all sites route through setSelection rather than calling
screen.select directly, including the mouse paths that previously
bypassed it for clipboard timing.
The new setSelectionAndCopy extends setSelection with the additional
'copy_on_select' behavior.
On macOS, this posts .ghosttySelectionDidChange, which is debounced
before posting a NSAccessibility .selectedTextChanged notification.
GTK has no consumer yet and no-ops the action.
See: #9932
PageList.resize takes the .lt branch when columns shrink, which calls
resizeWithoutReflow (mutating self.rows to the new smaller value) and
then resizeCols with the original opts.cursor.y. When both axes shrink
in one call and the cursor sits at or past the new bottom row, the
expression `self.rows - c.y - 1` underflows and panics in safety builds.
Use saturating subtraction; "remaining rows below cursor" is 0 once the
cursor sits at or past the new bottom.
This problem is reported by
[discussion#12905](https://github.com/ghostty-org/ghostty/discussions/12905)
Adds a PageList regression test exercising the underflow path fixed in
7fa6fffbc, and a libghostty-vt C API test mirroring the original repro
through ghostty_terminal_resize.
PageList.resize takes the .lt branch when columns shrink, which calls
resizeWithoutReflow (mutating self.rows to the new smaller value) and
then resizeCols with the original opts.cursor.y. When both axes shrink
in one call and the cursor sits at or past the new bottom row, the
expression `self.rows - c.y - 1` underflows and panics in safety builds.
Use saturating subtraction; "remaining rows below cursor" is 0 once the
cursor sits at or past the new bottom.
The core had no signal to the apprt when the active selection changed,
so a consumer (e.g. a screen reader) kept reading a stale selection
until some unrelated query refreshed it.
This change adds a payload-less selection_changed action that's fired on
a selection state transition. The apprt reads the current selection
through the normal read path.
This consolidates selection state changes so the notification fires
consistently: all sites route through setSelection rather than calling
screen.select directly, including the mouse paths that previously
bypassed it for clipboard timing.
The new setSelectionAndCopy extends setSelection with the additional
'copy_on_select' behavior.
On macOS, this posts .ghosttySelectionDidChange, which is debounced
before posting a NSAccessibility .selectedTextChanged notification.
GTK has no consumer yet and no-ops the action.
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`).
`setSelection` captured the previous selection, then called
`Screen.select` (which deinits the previous selection's tracked pins),
then compared the new selection against the now-freed previous pin via
`sel.eql(prev)`. That read freed pin memory (use-after-free).
The comparison was a copy-on-select optimization ("only re-copy if the
selection changed"). Remove it rather than repair it because:
- It never fired correctly. It compared against freed memory, so the
shipped behavior was already "always copy".
- It can't be repaired by copying `prev`'s pin before `Screen.select`.
That fixes the use-after-free but not the logic: the call sites (e.g.
mouse drag release) pass a selection equal to the one already set, so a
working `eql` skip would suppress the very copy those sites exist to
perform. A correct optimization would have to compare against the
last-copied selection (before the mouse event mutated the live one),
which would require extra state.
- It isn't worth tracking that additional state. The copy runs once per
selection gesture (mouse up, double-click), which isn't in a hot path,
so skipping a redundant re-copy only saves a single clipboard write.
Removing the skip eliminates the use-after-free and keeps the behavior
consistent with what we've already been doing.
---
_AI Disclosure_: Claude Opus 4.8 found this in a review while I was
working on adjacent code.
setSelection captured the previous selection, then called Screen.select
(which deinits the previous selection's tracked pins), then compared the
new selection against the now-freed previous pin via `sel.eql(prev)`.
That read freed pin memory (use-after-free).
The comparison was a copy-on-select optimization ("only re-copy if the
selection changed"). Remove it rather than repair it because:
- It never fired correctly. It compared against freed memory, so the
shipped behavior was already "always copy".
- It can't be repaired by copying `prev`'s pin before Screen.select.
That fixes the use-after-free but not the logic: the call sites (e.g.
mouse drag release) pass a selection equal to the one already set, so
a working `eql` skip would suppress the very copy those sites exist to
perform. A correct optimization would have to compare against the
last-copied selection (before the mouse event mutated the live one),
which would require extra state.
- It isn't worth tracking that additional state. The copy runs once per
selection gesture (mouse up, double-click), which isn't in a hot path,
so skipping a redundant re-copy only saves a single clipboard write.
Removing the skip eliminates the use-after-free and keeps the behavior
consistent with what we've already been doing.
**Important: this DOES NOT hook up the glyph protocol to Ghostty or
libghostty. Its just the parser.**
This adds the core parse/encode for the still in-development and
experimental terminal glyph protocol:
https://github.com/raphamorim/rio/pull/1542
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`.
For DoS protection, the default limits any glyph-related APC command
size to 1 megabyte.
> [!WARNING]
>
> Since this protocol is still in development and discussion, there is
no promise the implementation will stay within Ghostty or that any of
the APIs exposed by this will remain stable. We're just getting ahead of
it.
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`.