Commit Graph

9428 Commits

Author SHA1 Message Date
Zongyuan Li
002fd41429 lib-vt: add pwd_changed callback for OSC 7/9/1337
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.
2026-06-08 10:22:11 +08:00
Mitchell Hashimoto
7092b39445 GTK: Improve Split Close Behavior (#11173)
## 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
2026-06-06 14:02:16 -07:00
Mitchell Hashimoto
a979b8698b terminal: hook up glyph protocol glossary to terminal state
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.
2026-06-05 15:36:52 -07:00
Lukas
1e63834cdc fix(terminal): avoid integer overflow in selectPrev with no active matches 2026-06-05 23:30:58 +02:00
Claude Opus 4.8
bd365e1aa9 terminal: cover selection drop when all matches disappear
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.
2026-06-05 23:21:27 +02:00
Lukas
d6494544cf terminal: add panic test for integer overflow in selectPrev with no active matches 2026-06-05 23:15:15 +02:00
Mitchell Hashimoto
5a1edfb254 fix(terminal): guard wrap count when resize pushes cursor to scrollback (#12935)
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", .{});
}
```
2026-06-05 13:56:58 -07:00
Mitchell Hashimoto
179fe571d7 terminal: Glyph Protocol Glossary and request handler implementation (#12930)
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.
2026-06-05 13:55:42 -07:00
Mitchell Hashimoto
aab0f8079f Revert "font: add exact glyph protocol constraints"
This reverts commit b661adad2e.
2026-06-05 13:44:43 -07:00
Mitchell Hashimoto
b661adad2e font: add exact glyph protocol constraints
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.
2026-06-05 13:37:10 -07:00
Mitchell Hashimoto
e45f002d1a terminal/apc: reject malformed glyph register input
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.
2026-06-05 13:19:00 -07:00
Mitchell Hashimoto
f0d81f15ee terminal/apc: reject malformed glyph clear cp
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.
2026-06-05 13:13:04 -07:00
Mitchell Hashimoto
05eeb43942 font: exclude libghostty-vt from embedded font tests 2026-06-05 13:09:09 -07:00
Claude Opus 4.8
7837563ed6 fix(terminal): guard wrap count when resize pushes cursor to scrollback
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.
2026-06-05 21:03:11 +02:00
Mitchell Hashimoto
1c0aac54bd font: reshuffle glyph sizing types to Glyph.zig 2026-06-05 06:58:32 -07:00
Mitchell Hashimoto
d271b271f9 terminal/glyph: query 2026-06-05 06:39:22 -07:00
Mitchell Hashimoto
6f83d8a4f1 terminal/glyph: clear 2026-06-05 06:34:53 -07:00
Mitchell Hashimoto
cc91940993 terminal/glyph: register request 2026-06-05 06:32:41 -07:00
Mitchell Hashimoto
0cd815f94a terminal/apc: glyph glossary delete, contains, clear 2026-06-05 06:23:48 -07:00
Mitchell Hashimoto
cf548a3aad terminal/apc: glyph glossary registration business logic 2026-06-04 18:44:44 -07:00
Mitchell Hashimoto
59d2ad9b6a terminal: glyph protocol Glossary entry starting to take shape 2026-06-04 18:23:08 -07:00
Mitchell Hashimoto
2055bb6dd6 terminal: glyph request glyf decode 2026-06-04 12:02:13 -07:00
Mitchell Hashimoto
42fcd58dba libghostty-vt: add options to configure default cursor's style and blink (#12900)
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`).
2026-06-04 11:39:50 -07:00
Mitchell Hashimoto
8fcead00e5 font: glyf outline decoder and rasterizer (#12893)
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
2026-06-04 11:04:19 -07:00
Mitchell Hashimoto
52368cbcff core: send selection_changed notification (#12902)
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
2026-06-04 11:03:59 -07:00
Zongyuan Li
f135b95098 terminal: test shrinking both axes with cursor past new bottom
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.
2026-06-04 00:01:24 +08:00
zongyuan.li
7fa6fffbca terminal: saturate cursor subtraction in resizeCols
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.
2026-06-03 14:36:52 +08:00
Jon Parise
c4e1ab8883 core: send selection_changed notification
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.
2026-06-02 19:37:49 -04:00
Riccardo Mazzarini
e7b506c69d Test setting/resetting the default cursor style and blink 2026-06-02 18:12:17 +02:00
Riccardo Mazzarini
66950a4a53 libghostty: add option to set default cursor blink
Adds an option to `libghostty-vt` to configure whether the default
cursor displayed when an app sends a DECSCUSR reset sequence should
blink.
2026-06-02 18:03:45 +02:00
Riccardo Mazzarini
2444e4d557 libghostty: add option to set default cursor style
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`).
2026-06-02 17:57:41 +02:00
Mitchell Hashimoto
51995a7822 font: glyf rasterization png comparison 2026-06-02 06:08:59 -07:00
Mitchell Hashimoto
6246c288ae core: fix use-after-free in Surface.setSelection (#12894)
`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.
2026-06-02 06:07:31 -07:00
Mitchell Hashimoto
8eff74ef76 font: add glyf rasterizer 2026-06-01 20:38:02 -07:00
Jon Parise
76b9bdb199 terminal: test Screen.select frees existing pins 2026-06-01 20:09:25 -04:00
Jon Parise
ab82b8ab72 core: fix use-after-free in Surface.setSelection
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.
2026-06-01 20:09:14 -04:00
Mitchell Hashimoto
d8f56b790e font: add glyf entry decoder to outline
Add Glyf.Outline for decoding the contours and points of a Glyf.
2026-06-01 14:52:23 -07:00
Mitchell Hashimoto
d3775d1ed0 terminal: glyph protocol parser and response encoder
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`.
2026-06-01 10:50:05 -07:00
masterflitzer
a181c386ca config: fix missing space in docs
fixes #12873

comment/docs only change:
switched space and tab in default value of `selection-word-chars`
so there is no space at the value boundary
needed because markdown trims spaces at the beginning & end
of a code snippet
2026-05-31 18:23:29 +02:00
Jeffrey C. Ollie
2c62d182ce gtk: fix context menu hiding quick-terminal (#12843)
Fixes #12783 where opening the context menu (with right click) inside
the quick-terminal will hide the quick-terminal if autohide is enabled.

The cause of this issue is the quick-terminal window becoming inactive
and immediately active again when you open the context-menu. When the
window becomes inactive, the autohide feature hides the quick-terminal.
The temporary focus loss in GTK is triggered by GDK focus change events,
which probably originate from the windowing backend treating the context
menu as its own window. Whereas in GTK the context menu is not a
separate window but instead part of the widget tree of the window it was
opened from, so even when the context menu has focus that window is
still the active one in GTK.

As a fix `Window.propIsActive`, which implements the autohide logic,
will now do its work from a timeout callback, since there is probably no
reliable way to distinguish a temporary focus loss from a real one from
inside GTK and I'm not sure we can make any assumptions about the timing
of things happening in the windowing backend. A 100ms delay should be
long enough for the focus state to settle while still hiding the
quick-terminal quickly.

I reproduced the bug and verified the fix on Wayland with both Hyprland
and KDE. Temporary focus loss happens on X11+KDE as well, although it
doesn't matter there because there is no quick-terminal.

### AI Disclosure

No AI was used, code and comments were written by myself.
2026-05-29 22:44:30 -05:00
Daniel Kinzler
ff963f3119 Renamed timeout source and callback function. Added comment explaining timeout delay. 2026-05-29 17:40:25 +02:00
Mitchell Hashimoto
90175950d5 libghostty-vt: preserve shell prompts on resize by default (#12653)
This PR makes libghostty-vt preserve shell prompts across resize unless
the shell explicitly opts into prompt clearing/redraw with `redraw=1`.
2026-05-29 06:41:23 -07:00
Mitchell Hashimoto
519a612beb libghostty: fix wasm build for selection gesture 2026-05-28 13:00:49 -07:00
Mitchell Hashimoto
3cf01e8445 libghostty: add utf-8 grapheme cell getter to C API
Add a render-state row-cells getter that encodes the current cell's
full grapheme cluster directly as UTF-8 into a caller-provided
GhosttyBuffer. The getter writes the base codepoint first, followed by
any extra grapheme codepoints, and follows the existing buffer-writer
convention where len is bytes written on success or required capacity
on GHOSTTY_OUT_OF_SPACE.

Previously C consumers could query grapheme codepoints, but bindings
that needed UTF-8 text had to reconstruct and encode the cluster
themselves. That duplicated terminal internals in downstream bindings
and made users pay for awkward cross-language struct handling. By
owning the UTF-8/grapheme behavior in libghostty, bindings can use one
stable C API and optionally wrap it with small binding-local helpers.
2026-05-28 12:33:36 -07:00
Daniel Kinzler
1753d57bfd remove timeout source when window is disposed 2026-05-28 15:08:12 +02:00
Mitchell Hashimoto
8beea5f92d libghostty: expose row cell styling bit
Add a render row-cells data key for querying whether the current cell has
explicit styling. This lets consumers avoid fetching a raw cell or full style
snapshot when all they need is the cell's HasStyling bit.

The new key is appended to the existing enum for ABI safety and is served by
the existing row-cells getter path. Existing data keys and function exports are
unchanged.
2026-05-27 21:10:26 -07:00
Mitchell Hashimoto
f730ee0557 libghostty: expose viewport active state
Expose whether the terminal viewport is currently pinned to the active
area through the libghostty-vt terminal data API. Previously embedders
could only infer this from scrollbar geometry, which was indirect and
could require the more expensive scrollbar calculation.

The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact
PageList viewport state as a bool. The scroll viewport test now verifies
the value while moving between the active area and scrollback.
2026-05-27 15:24:49 -07:00
Mitchell Hashimoto
4e2d7c314b libghostty: optimize bits for selection gesture validation fields 2026-05-27 11:05:33 -07:00
Mitchell Hashimoto
f0fcb10406 libghostty: selection gesture deep press 2026-05-27 10:57:50 -07:00
Mitchell Hashimoto
603684ba11 libghostty: selection gesture autotick 2026-05-27 10:55:15 -07:00