Commit Graph

16605 Commits

Author SHA1 Message Date
Mitchell Hashimoto
a5071a9e6e terminal: skip full bitmap prefixes on allocation
Every allocation previously restarted its free-run search at the first
bitmap. Dense grapheme and string allocators repeatedly rescanned the
same full leading words, making churn grow with the size of occupied
storage.

Track the first bitmap that may contain free chunks. Allocation
advances the hint over full words and free lowers it, preserving exact
first-fit behavior. A 32-bit offset index occupies existing padding, so
the allocator remains 24 bytes on 64-bit targets.

ReleaseFast medians use seven sequential runs after two warmups and
report user CPU time. Synthetic grapheme churn uses the production 80%
map limit unless noted.

| entries | chunks | occupancy | loops | before | after | change |
|---:|---:|---:|---:|---:|---:|---:|
| 4,096 | 1 | 80% | 2,560 | 410.475 ms | 311.512 ms | -24.1% |
| 16,384 | 1 | 80% | 640 | 806.146 ms | 499.491 ms | -38.0% |
| 32,768 | 1 | 80% | 256 | 895.591 ms | 477.662 ms | -46.7% |
| 16,384 | 2 | 80% | 640 | 1.298 s | 171.429 ms | -86.8% |
| 16,384 | 1 | 25% | 4,096 | 425.012 ms | 239.661 ms | -43.6% |

The same parent and child binaries were measured through the real VT
stream benchmark using fixed grapheme corpora.

| VT workload | before | after | change |
|---|---:|---:|---:|
| grapheme, 1 extra codepoint | 365.210 ms | 238.915 ms | -34.6% |
| grapheme, 5 extra codepoints | 430.772 ms | 301.518 ms | -30.0% |
| grapheme, 32 extra codepoints | 1.517 s | 314.785 ms | -79.2% |
2026-07-12 07:01:56 -07:00
Mitchell Hashimoto
4f35a0614e terminal: fold bitmap run searches logarithmically
Finding an in-word run of n free chunks previously intersected the
bitmap with every shift from one through n minus one. Large grapheme
allocations therefore performed up to 63 dependent shift-and-mask steps
for every word examined.

Fold each intermediate result by its represented run length instead.
This preserves the cross-word first-fit fallback while reducing an
in-word search to logarithmic steps.

ReleaseFast medians use nine sequential runs after two warmups and
report user CPU time. Grapheme churn uses 256 cells at the production
80% map limit; loop counts are calibrated per row.

| extra codepoints | chunks | loops | before | after | change |
|---:|---:|---:|---:|---:|---:|
| 1 | 1 | 65,535 | 289.506 ms | 289.121 ms | -0.1% |
| 5 | 2 | 65,535 | 210.946 ms | 188.005 ms | -10.9% |
| 13 | 4 | 50,000 | 234.318 ms | 192.031 ms | -18.0% |
| 29 | 8 | 25,000 | 207.009 ms | 142.711 ms | -31.1% |
| 125 | 32 | 3,000 | 157.674 ms | 71.398 ms | -54.7% |
| 253 | 64 | 800 | 165.088 ms | 43.548 ms | -73.6% |

Real VT stream medians use seven sequential runs after two warmups.

| workload | before | after | change |
|---|---:|---:|---:|
| VT grapheme, 5 extra codepoints | 448.057 ms | 429.095 ms | -4.2% |
| VT grapheme, 32 extra codepoints | 2.005 s | 1.514 s | -24.5% |
2026-07-12 07:01:56 -07:00
Mitchell Hashimoto
0f50fdf6ce terminal: bound grapheme map probe lengths
The grapheme map previously allowed its open-addressed table to fill
completely. Inserts and misses therefore degraded into full-table scans
as grapheme storage approached capacity.

Limit usable slots to eighty percent of the raw table. Reaching the
limit grows the page before probes become pathological without doubling
grapheme map metadata on pages that contain no graphemes. The benchmark
caps its resident set at the same production limit.

Exact row compaction now derives its minimum byte request from the same
allocator-to-map layout helper used by Page.layout. This prevents layout
policy changes from under-sizing a compacted map. A sweep over 1 through
4,096 entries pins the relationship.

ReleaseFast medians use eleven sequential runs after two warmups and
report user CPU time. Rows above the 80% boundary compare the same
resident entry count before and after the new policy grows the page.

| pre-growth occupancy | before | after growth | change |
|---:|---:|---:|---:|
| 80% | 79.622 ms | 78.454 ms | -1.5% |
| 90% | 155.778 ms | 26.572 ms | -82.9% |
| 94% | 216.450 ms | 15.096 ms | -93.0% |
| 100% | 688.143 ms | 5.754 ms | -99.2% |

Real VT medians use seven sequential runs after two warmups.

| workload | before | after | change |
|---|---:|---:|---:|
| ASCII, 219 MiB | 225.927 ms | 227.741 ms | +0.8% |
| grapheme, 1 extra codepoint | 361.388 ms | 368.598 ms | +2.0% |
| grapheme, 5 extra codepoints | 449.764 ms | 456.409 ms | +1.5% |
| grapheme, 32 extra codepoints | 2.024 s | 2.024 s | +0.0% |

A standard production page retains 512 raw map slots, a 10,768-byte
map buffer, and a 409,600-byte total layout. Its usable map limit moves
from 512 to 409 entries, so capacity grows before the final allocator
chunks are consumed.
2026-07-12 07:01:55 -07:00
Mitchell Hashimoto
762457c89f terminal: align page bitmap capacities
Bitmap allocators reserve complete 64-chunk words, but Page recorded
the smaller requested byte counts. Doubling a small capacity could
therefore recreate the same layout and make an immediate allocation
retry fail again.

Size the grapheme map directly from the rounded allocator raw slot
capacity and record the actual grapheme and string byte capacities. Page
layout is now idempotent and each capacity increase advances to larger
storage.
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
5f16939905 benchmark: add dense style replacement workload
Add a replacement mode that keeps all but one requested style live
while repeatedly adding and releasing unique styles. This isolates
dense backward-shift deletion and ID reuse without triggering the
all-dead bulk reset.

Generate replacement styles on demand so long runs continue to
exercise misses instead of cycling through the resident working set.
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
3267b63ef9 terminal: preserve set probe invariants
Only reap a dead RefCountedSet occupant when the held item has an equal
or longer probe sequence. Replacing a dead item with a shorter PSL lowers
the value stored in that bucket and makes later live entries unreachable
through the Robin Hood early-exit rule.

Add a deterministic reproduction and a high-load randomized oracle. The
failure could otherwise silently duplicate styles or hyperlinks under new
IDs and increase page capacity pressure.
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
58be0f13d8 terminal: expand RefCountedSet unit coverage
Add direct coverage for reference counting, dead-ID reuse, addWithId,
full-set behavior, and rehash requests. These paths previously depended on
indirect screen and page tests.

Add a test-only integrity verifier that cross-checks table buckets, probe
distances, PSL statistics, liveness, and lookup reachability after each
scenario.
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
e93c23f4dd benchmark: add grapheme map workloads
Add page-backed lookup and churn workloads for grapheme storage. The
benchmark exercises the cell offset map together with the bitmap allocator,
including configurable allocator load and multi-chunk grapheme values.

Expose the grapheme chunk dimensions so the harness can size page storage to
an exact working set without duplicating production constants.
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
49cfb347b5 terminal: advance set lookup buckets directly
Carry the current RefCountedSet bucket through the probe loop instead of
recomputing hash plus probe length and narrowing it on every iteration.
The bucket remains in the set ID type and wraps with the existing table mask.

ReleaseFast timings are seven-run median user CPU times.

| Workload | Before | After | Change |
| --- | ---: | ---: | ---: |
| 128 styles, live lookup | 0.71s | 0.70s | -1.4% |
| 4,096 styles, live lookup | 0.89s | 0.85s | -4.5% |
| Alternating SGR stream | 1.11s | 1.11s | unchanged |
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
62cc056ae3 terminal: reset dead set entries in bulk
Reset the RefCountedSet table in one pass when every stored item is dead.
The previous add path deleted trailing entries individually, repeatedly
backshifting probe sequences even though no live entry needed to survive.

Deletion callbacks still run exactly once before storage is cleared, so
page-owned hyperlink values retain their existing ownership semantics.

ReleaseFast timings are seven-run median user CPU times.

| Workload | Before | After | Change |
| --- | ---: | ---: | ---: |
| 128 styles, dead churn | 0.72s | 0.60s | -16.7% |
| 4,096 styles, dead churn | 0.77s | 0.66s | -14.3% |
| Clear/redraw SGR stream | 0.65s | 0.61s | -6.2% |
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
b8458383c9 terminal: reuse hashes across set insertion
Compute a RefCountedSet value hash once and pass it through lookup and
insertion. A miss previously hashed before probing and then repeated the
same work when insertion began. The shared private lookup path also keeps
zero-capacity contexts from being evaluated before their backing data exists.

ReleaseFast timings are seven-run median user CPU times.

| Workload | Before | After | Change |
| --- | ---: | ---: | ---: |
| 128 styles, live lookup | 0.75s | 0.69s | -8.0% |
| 128 styles, dead churn | 0.79s | 0.72s | -8.9% |
| 4,096 styles, dead churn | 0.85s | 0.77s | -9.4% |
| Alternating SGR stream | 1.13s | 1.11s | -1.8% |
| Clear/redraw SGR stream | 0.67s | 0.65s | -3.0% |
2026-07-12 06:58:39 -07:00
Mitchell Hashimoto
b0cf99cf9a terminal: reduce style hash mixing cost
Use the two-round SplitMix64 finalizer for the folded PackedStyle key.
The previous std.hash.int u64 mixer performs three multiply rounds. Style
hashing is paid for every real SGR transition, and the additional round did
not improve lookup behavior for this key distribution.

ReleaseFast timings are seven-run median user CPU times.

| Workload | Before | After | Change |
| --- | ---: | ---: | ---: |
| 4,096 styles, live lookup | 0.87s | 0.83s | -4.6% |
| Alternating SGR stream | 1.20s | 1.13s | -5.8% |
| Truecolor SGR stream | 0.68s | 0.67s | -1.5% |
| Clear/redraw SGR stream | 0.68s | 0.67s | -1.5% |
2026-07-12 06:58:38 -07:00
Mitchell Hashimoto
498533d663 terminal: avoid copying set lookup items
Keep the resident RefCountedSet item behind a pointer while probing. The
previous value copy materialized the complete generic item before checking
its metadata and equality, which is especially costly for terminal styles.

ReleaseFast timings are seven-run median user CPU times.

| Workload | Before | After | Change |
| --- | ---: | ---: | ---: |
| 128 styles, live lookup | 0.80s | 0.74s | -7.5% |
| 4,096 styles, live lookup | 0.92s | 0.87s | -5.4% |
| Alternating SGR stream | 1.21s | 1.20s | -0.8% |
2026-07-12 06:58:38 -07:00
Mitchell Hashimoto
c68d27c5fa benchmark: add style set workloads
Add focused live-lookup and all-dead churn workloads for the terminal
style set. The benchmark uses a real page-backed set so its layout and
offset access match the production path.

The lookup mode repeatedly acquires existing styles while preserving their
liveness. The churn mode releases the complete working set between passes
to model clear and redraw behavior.
2026-07-12 06:58:38 -07:00
Mitchell Hashimoto
641df6cd2a terminal: reuse resident explicit hyperlinks
Starting an OSC 8 hyperlink copied its ID and URI into page memory before
the ref-counted set could determine that the same live entry already
existed. Repeated explicit IDs therefore paid allocation, copy, and free
costs for data that was immediately discarded.

Add adapted lookup support to RefCountedSet and let decoded hyperlinks
hash and compare directly against page-resident entries. Probe explicit
IDs before allocating strings and increment the existing reference on a
match. Implicit IDs remain on the original path because they are generated
uniquely and an early probe would always miss.

An explicit and implicit parity test pins decoded and resident hashes to
the same bit pattern so the adapted lookup cannot silently miss after
either implementation changes.

This also lets a live link be reused when the page string allocator has no
free chunks. The old path requested a transient string copy first and
could force an unnecessary page-capacity increase before de-duplicating.

ReleaseFast medians use seven sequential runs after two warmups and
report user CPU time. The stream reasserts one live explicit link 25,000
times while overwriting the same cell.

| workload | before | after | change |
|---|---:|---:|---:|
| repeated explicit link | 437.925 ms | 438.051 ms | +0.0% |

The isolated throughput effect is neutral; this change removes transient
allocation and capacity growth rather than improving the steady-state
parser path.
2026-07-12 06:58:30 -07:00
Mitchell Hashimoto
9328b67201 terminal: specialize page offset hashing
Page-local grapheme and hyperlink maps hashed 32-bit cell offsets through
the generic byte-oriented Wyhash path. Hashing is paid on every lookup,
insert, move, and erase even though the key is already an integer.

Add an offset context that widens the key before applying the integer
mixer. Widening preserves entropy in the high bits used by the map
fingerprint, while avoiding the generic byte hashing path. Use the
specialized context for both cell-offset maps.

ReleaseFast medians use seven sequential runs after two warmups and
report user CPU time. Each pass visits a 4,096-entry hyperlink map.

| workload | working-set load | passes | before | after | change |
|---|---:|---:|---:|---:|---:|
| lookup | 100% | 40,000 | 478.518 ms | 313.179 ms | -34.6% |
| remove/insert churn | 50% | 5 | 491.084 ms | 383.838 ms | -21.8% |
2026-07-11 22:23:03 -07:00
Mitchell Hashimoto
a797a97834 terminal: fix RefCountedSet capacity boundaries
RefCountedSet used maxInt(Id) both as an empty bucket marker and as
the final valid bucket in its largest table. A dead-ID reuse sequence at
maximum capacity could therefore treat a reset item as table-resident.
The floating-point capacity calculation could also produce 65,536 for a
u16 page capacity.

Mark empty items with the probe sequence field, whose valid range is
already capped below maxInt, and calculate the 13/16 load factor exactly.
Represent the largest table request as maxInt(Id), which Layout.init
rounds to the required power of two. Add maximum-capacity regressions and
a randomized live-value oracle for ID reuse and displacement.
2026-07-11 19:49:19 -07:00
Mitchell Hashimoto
dcfaa92553 terminal: fix bitmap allocator word boundaries
Bitmap allocations of at most 64 chunks only searched within a single
bitmap word. Contiguous free space crossing a word boundary was therefore
invisible and could force an unnecessary page growth. Larger requests
could also advance past the final bitmap before returning out of memory.

Search adjacent words for small cross-boundary runs and guard the final
word in the large-allocation path. Add boundary regressions and a
randomized oracle that verifies both placement and bitmap mutation.
2026-07-11 16:37:58 -07:00
Mitchell Hashimoto
a887df42c5 terminal: bound page map probe lengths without tombstones (#13294)
Replaces #13292

This optimizes page-local hyperlink and grapheme maps under full
occupancy, repeated clear and redraw, and managed-cell movement.

Hyperlink maps previously allowed 100% occupancy and retained tombstones
after removals. This PR bounds probe lengths with a reserved load factor
and removes tombstones from the design entirely: removal now restores
the table to the exact state it would be in had the key never been
inserted, so fragmentation cannot accumulate by construction.

Clear and redraw improves by 2.4x, hyperlinked cell movement by 10.4x, a
scrolling OSC 8 stream by 1.7x, and normal ASCII/unicode output does not
regress.

## The changes

1. **Reserve probe headroom in hyperlink cell maps.** Hash maps now
support a maximum load config and hyperlink maps set it to 80%.
**Result: known-absent cell movement improves from 1.550 s to 149 ms.**

2. **Replace tombstone removal with backward-shift deletion.** Removing
an entry now closes the hole by shifting later entries backward. This
keeps probe chains short without tombstones, headroom bookkeeping, or
periodic rehashing. Removal may move other entries, but no caller
retains pointers across removals. **Result: clear and redraw improves
from 672 ms to 512 ms over the tombstone revision, 2.4x over baseline,
and the map shrinks by ~140 lines, improving maintenance.**

3. **Avoid duplicate probes when moving managed cell data.** Hyperlink
and grapheme movement already guarantees that the destination is absent,
so these paths use no-clobber insertion and skip duplicate detection.
With backward-shift deletion this fast path is safe at every load state
without special cases, because removing the source genuinely frees a
slot before the destination is inserted. **Result: movement stays at
parity with the tombstone revision (149 ms vs 148 ms) while its
rehash-retry guard is deleted.**

Note a trade-off: the map-churn microbenchmark at maximum load is ~16%
slower, because removal pays a cluster scan up front instead of
deferring cost to later probes and periodic rebuilds. But its a
synthetic case that isn't grounded in reality. Instead, the realistic
counterpart (clear and redraw at high occupancy through the full VT
stream) is 31% faster, and lookups are neutral.

## Benchmarks

| workload | baseline | tombstones | this PR |
|---|---:|---:|---:|
| clear and redraw, 3,000 frames | 1.243 s | 671.9 ms | 511.6 ms |
| scrolling OSC 8 stream, 39 MiB | 5.375 s | 3.484 s | 3.214 s |
| known-absent cell movement | 1.550 s | 148.2 ms | 149.2 ms |
| map churn, 50% load | n/a | 566.8 ms | 464.6 ms |
| map churn, max load | n/a | 1.088 s | 1.259 s |
| map lookup | n/a | 636.2 ms | 643.0 ms |

## LLM Notes

Assisted by GPT 5.6 and Fable. Hand-reviewed, transition to backshift
was my nudge, benchmark numbers are from an LLM benchmark run using
committed benchmarks.
2026-07-11 14:47:37 -07:00
Mitchell Hashimoto
fedd42e8d6 terminal: use backward-shift deletion in page maps
Page maps handled removal with tombstones, which a fixed-capacity map
can never outgrow. Keeping probe lengths bounded required an insertion
headroom counter, an allocation-free rehash, and five separate recovery
paths with subtle invariants: removal created a tombstone without
restoring headroom, so the counter could reach zero while the map was
half empty and every insertion path had to be prepared to rebuild.

Replace tombstones with backward-shift deletion (Knuth vol. 3, 6.4
algorithm R). Removal restores the table to the state it would be in
had the key never been inserted, so probe chains stay canonical at all
times and fragmentation cannot accumulate by construction. This deletes
the headroom counter, the in-place rehash, and every recovery path.
Insertion no longer invalidates pointers; removal may now move other
entries instead, and no caller holds entry pointers across removals.

Removal costs a cluster scan instead of a byte write, paying
incrementally what tombstones deferred to later probes and periodic
rebuilds. Free slots remain all-zero bytes so probe loops keep fusing
the state and fingerprint checks into single-byte compares. A
randomized oracle test against the stdlib map and a canonical-placement
invariant check cover the new removal path, including full tables where
no free slot terminates the shift.

ReleaseFast benchmarks against the prior map commits:

| workload | delta |
|---|---:|
| map churn, 50% load | 1.23x faster |
| map churn, 75% load | 1.15x faster |
| map churn, max load | 1.07x slower |
| map lookup | neutral |
| clear and redraw stream | 1.32x faster |
| full OSC 8 stream | 1.06x faster |
| linked-line movement | neutral |
2026-07-11 14:23:45 -07:00
kat
d31ac2be38 i18n: Fix untranslated Close Split string in zh_CN.po (#13296)
Fix one untranslated string in the Simplified Chinese localization
(`zh_CN.po`).

## Changes

- `msgid "Close Split"`: `""` → `"关闭分屏"`

This achieves terminology consistency:
- Split → 分屏 (already established)
- Close Split? → 关闭分屏吗? (already existed)
- Close Split → 关闭分屏 (this fix)
2026-07-11 18:20:03 +00:00
ghostty-vouch[bot]
78a9731c9d Update VOUCHED list (#13299)
Triggered by
[comment](https://github.com/ghostty-org/ghostty/issues/13296#issuecomment-4948155117)
from @00-kat.

Vouch: @Arvin7liu

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-11 17:50:36 +00:00
Arvin7liu
ae76cfbfcf i18n: fix untranslated Close Split string in zh_CN.po 2026-07-12 00:43:44 +08:00
Tim Culverhouse
65f953e8e8 terminal: avoid duplicate probes when moving cell data
Hyperlink and grapheme maps are keyed by cell offset. Moving their
data removes the source entry and reinserts it at a destination known
to be absent. Generic clobbering insertion still searches the probe
sequence to rule out a duplicate.

Use no-clobber insertion for these moves so the first available
tombstone can be reused. If a destination reaches a free slot after
tombstones exhaust insertion headroom, rebuild the map in place and
retry before decrementing the budget. This keeps the known-absent fast
path safe at every load state.

The ReleaseFast movement benchmark improves from 133.7 ms to 126.4 ms,
a 5.5% reduction. The linked-line control remains neutral at 395.4 ms
before and 395.7 ms after.
2026-07-11 09:21:38 -07:00
Mitchell Hashimoto
7e14347c13 terminal: bound page map probe lengths
#13292

Page maps previously allowed a 100% load factor. Once live entries
and tombstones filled every slot, a missing-key lookup or insertion
could scan the entire map.

Reserve 20% of each offset hash map as insertion headroom and track
that budget separately from the live count. Port the allocation-free
in-place rehash from Zig HashMapUnmanaged so canonical insertion can
rebuild fragmented probes and restore tombstone-exhausted headroom
without growing the page. Assumed-capacity insertion now applies the
same guard, preventing the headroom counter from wrapping.

Pass the exact requested hyperlink count into map layout before
load-factor scaling, avoiding a redundant power-of-two rounding step.
Screen-level recovery now grows only when live hyperlinks actually fill
usable capacity.

ReleaseFast terminal benchmarks compare main with the combined map
changes:

| workload | before | after | speedup |
|---|---:|---:|---:|
| map churn | 1.253 s | 12.5 ms | 100x |
| full OSC 8 stream | 3.576 s | 75.6 ms | 47x |
| clear and redraw | 299.1 ms | 118.3 ms | 2.5x |

Co-authored-by: Tim Culverhouse <tim@timculverhouse.com>
2026-07-11 09:21:27 -07:00
Mitchell Hashimoto
53bd14fecf terminal: update page serial to be treated as a generation marker (correctness) (#13282)
This changes our `page_serial` semantics from monotonic min to a
generation marker (any change means you should reload). This fixes a
number of real bugs that search had.

In reality, this invariant was already broken and not true. So this PR
comes to reality with that and fixes all our usage. The most visible
failures were panics when selecting cached search results after
partially erasing a history page or splitting a page, but the same bug
class affected viewport search fingerprints and asynchronous render
highlights.

It was mostly search, for now, but this type of fix is important for
some upcoming work I'm playing around with regarding deferred reflow and
disk offload.

## Page Serial Semantics

`Node.serial` is now explicitly a page generation. It changes whenever a
node is allocated or reused, and whenever an in-place mutation changes
the meaning or valid range of its row coordinates.

`page_serial_min` is renamed to `page_serial_epoch` to reflect its
remaining purpose. Only a whole-list reset advances the epoch. A serial
below the epoch is definitely stale and can be rejected in O(1); a
serial at or above it is only potentially valid and is checked against
the live list using its captured pointer and generation.

## Performance

The generation bump is constant work per node whose layout is already
being changed. There is no additional page-list scan on normal row
append or scrollback pruning.

I did add a new field to the render state, so I ran render state
benchmarks we already have that shows a very slight slowdown but this is
acceptable especially given recent speedups outweigh this significantly:

| render measurement | before | after | change |
|--------------------|--------|-------|--------|
| full rebuild | 2.915 us/update | 2.926 us/update | +0.27% |
| lock-held rebuild | 2.472 us/update | 2.489 us/update | +0.53% (within
noise) |
| clean update | 19.03 ns/update | 19.30 ns/update | +1.43% (+0.27 ns) |
| one dirty row | 52.97 ns/update | 53.40 ns/update | +0.96% (within
noise) |
| flattened highlight miss | 86.77 ns/update | 86.87 ns/update | +0.19%
(within noise) |
| flattened highlight match | 88.38 ns/update | 90.26 ns/update | +2.00%
(+1.88 ns) |

## LLM Notes

This work was done in concert with Codex. I reviewed and reshaped the
serial model, implementation, tests, documentation, and performance
analysis throughout. This PR message is hand-written.
2026-07-10 09:54:42 -07:00
Mitchell Hashimoto
89eca063e9 terminal/search: avoid pruning history on reload
reloadActive previously pruned every cached history result whenever the
active area changed. Search reconciliation runs under the terminal lock,
so this scaled with both result and page counts on a frame-paced path.

Validate only the selected history result during reload. Keep full
pruning before selection navigation so stale candidates are removed
before their coordinates are tracked.
2026-07-10 09:50:40 -07:00
Mitchell Hashimoto
0bad91adb8 terminal: renew generations for direct row shifts
Screen scroll fast paths and Terminal line insertion/deletion move Row
values directly instead of using PageList erasure helpers. Their node
generations therefore stayed valid after cached row coordinates changed.

Expose the PageList layout invalidator to sibling terminal modules and
renew each affected existing page once before full-row rotations or
swaps. Keep partial-width cell moves on the content-only path and cover
same-page, cross-page, and fresh-tail behavior.
2026-07-10 09:29:11 -07:00
Mitchell Hashimoto
d4ac93a039 terminal: add clipboard_set effect for OSC 52 clipboard writes (#13182)
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.
2026-07-10 09:27:21 -07:00
Mitchell Hashimoto
634ef71986 terminal: make clipboard writes protocol neutral
#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.
2026-07-10 09:17:59 -07:00
Mitchell Hashimoto
f6d9a582e4 terminal: rename page serial floor as epoch
page_serial_min no longer represented the minimum live page serial.
Its name still suggested an ordering relationship and obscured the
remaining reset-only invalidation behavior.

Rename the field to page_serial_epoch and document its O(1) rejection
and ScreenSearch bulk-pruning utility. Explicitly verify that ordinary
bounded pruning does not begin a new whole-list epoch.
2026-07-10 08:29:31 -07:00
Mitchell Hashimoto
4aa3c1051d terminal: vectorize APC payload scanning (#13281)
Kitty graphics payloads are dispatched in bulk, but finding each slice
boundary still examines every byte with a scalar loop. This leaves large
direct base64 image transmissions parser-bound.

Scan ordinary APC bytes using the vector width recommended for the
compile target. Keep the scalar scan both as the tail and as the full
fallback when the target has no recommended vector width. Test
state-machine boundaries against byte-at-a-time parsing.

A ReleaseFast APC parser benchmark over the same 64 MiB Kitty graphics
corpus, with 10 warmups and 30 measured runs, produced:

```
                  mean       median
  scalar        37.6 ms      32.6 ms
  vectorized    22.3 ms      19.0 ms
```
Hyperfine reports the vectorized version as **1.69 times faster
overall**, with the median runtime improving by approximately 42
percent.

AI Disclosure: This patch was created with the assistance of GPT-5.6
2026-07-10 08:11:30 -07:00
Mitchell Hashimoto
dd956f37cc terminal: renew generations for shifted rows
Single-row erasure and trailing blank-row trimming can remap or remove
stored row coordinates while retaining the same node. External pointer-
plus-generation references could therefore survive these less common
PageList mutation paths.

Renew each affected node inside the traversal that already changes it,
and signal compression activity only when row erasure begins in
history. This adds constant work per touched page without another list
scan or normal append cost.
2026-07-10 07:34:01 -07:00
Mitchell Hashimoto
d0a26d1460 terminal: reschedule compression after page replacement
Compaction and capacity growth publish fresh page generations, so an
active incremental traversal can detect them. They did not update the
separate activity token, however, and a completed compressor could
remain idle after either operation restored or replaced a cold page.

Mark successful replacements as compression activity without resetting
the exact continuation marker. The next scheduled step can retain valid
progress or restart through the existing generation checks.
2026-07-10 07:34:01 -07:00
Mitchell Hashimoto
a89c6133c5 terminal/search: validate history before reload
ScreenSearch reloaded active state before pruning stale flattened
history. Reload could compare a shifted tracked selection against
cached page coordinates and panic before the later validation step ran.

Prune history immediately after dimension reconciliation in
reloadActive, before any cached coordinate is inspected or converted to
a pin. Selection now relies on that ordering and avoids a redundant
second prune.
2026-07-10 07:34:01 -07:00
Mitchell Hashimoto
c9ad708ef7 terminal: validate flattened highlight generations
RenderState applied flattened highlights after releasing the terminal
lock and matched them to copied rows by node address alone. A recycled
address could therefore make a stale asynchronous highlight decorate
unrelated content.

Capture the live node generation with each render row and require both
the pointer and copied generation to match. Validation remains lock-
free and never dereferences a potentially stale node.
2026-07-10 07:34:01 -07:00
Mitchell Hashimoto
c243d89bdf terminal/search: include generations in viewport fingerprints
ViewportSearch compared only cached node addresses when deciding
whether to reuse its owned search window. An in-place layout change or
pool address reuse could therefore make stale text and coordinates
appear current.

Snapshot each node generation with its pointer and compare only those
captured values. This detects a renewed generation at the same address
without ever dereferencing cached node pointers.
2026-07-10 07:34:01 -07:00
Mitchell Hashimoto
83aada2056 terminal/search: preserve serial order in reverse matches
Reverse multi-page highlight construction reordered node and row-
bound columns but left captured page generations in their original
order. Every cross-page result therefore paired each node with another
page generation and could be rejected as stale despite remaining live.

Reverse the serial column with the other flattened chunk metadata and
cover the node-plus-generation pairing in the existing boundary match
test.
2026-07-10 07:34:00 -07:00
Mitchell Hashimoto
91a63e5287 terminal: invalidate split source page refs
PageList.split moved the source suffix into a fresh target but left
the shortened source on its old generation. Cached matches in that
suffix could therefore pass validation against the live source pointer
and reach pin tracking with invalid coordinates.

Renew the source generation only after target cloning succeeds, and
mark compression activity so restored history is reconsidered. The
conservative floor keeps bounded pruning safe even when the renewed
source and fresh target precede older successors.
2026-07-10 07:34:00 -07:00
Mitchell Hashimoto
a804e3ac78 terminal: invalidate partially erased page refs
Partial history erasure shifted retained rows while preserving the
node generation. Cached flattened matches then passed pointer-plus-
generation validation and could be tracked with coordinates beyond the
shortened page.

Renew the generation before reinitializing or shifting a page layout,
and mark compression activity so an incremental pass restarts and
revisits a restored cold page. The conservative serial floor permits
the fresh generation to remain before older live successors without
rejecting them.
2026-07-10 07:34:00 -07:00
Mitchell Hashimoto
f1a5fab452 terminal: keep page serial floor conservative
Page generations are not ordered with the page list because splits
and in-place replacements insert fresh generations before older live
pages. Advancing page_serial_min while pruning could therefore reject
live successors and fail PageList integrity checks.

Keep the floor as a whole-list invalidation epoch and use the existing
pointer-plus-generation membership check for ordinary removals. Add
bounded pruning coverage for split and replacement ordering, and verify
reset still rejects a stale generation when its node address is reused.
2026-07-10 07:34:00 -07:00
Tim Culverhouse
8c523ed039 terminal: vectorize APC payload scanning
Kitty graphics payloads are dispatched in bulk, but finding each slice
boundary still examines every byte with a scalar loop. This leaves large
direct base64 image transmissions parser-bound.

Scan ordinary APC bytes using the vector width recommended for the compile
target. Keep the scalar scan both as the tail and as the full fallback when
the target has no recommended vector width. Test state-machine boundaries
against byte-at-a-time parsing.

A ReleaseFast APC parser benchmark over the same 64 MiB Kitty graphics
corpus, with 10 warmups and 30 measured runs, produced:

                  mean       median
  scalar        37.6 ms      32.6 ms
  vectorized    22.3 ms      19.0 ms

Hyperfine reports the vectorized version as 1.69 times faster overall, with
the median runtime improving by approximately 42 percent.
2026-07-10 09:29:15 -05:00
Mitchell Hashimoto
3f2b7946d7 Misc runtime safety fixes (#13278)
All found by GPT 5.6. I'm still going through manual review of each one
now and will remove or rewrite the ones that are pointless or
unreachable (if any, I prompted it to ignore those too).
2026-07-10 06:58:02 -07:00
Mitchell Hashimoto
9b466e9c55 terminal: fix count-limited page iteration
Count-limited PageIterator traversal took the minimum of the page and requested lengths, then tested whether that minimum exceeded the request. The condition was impossible, so iteration never crossed a page. Reverse traversal also excluded the current row and subtracted one from row zero, causing a runtime safety panic.

Count the current row in both directions, consume the returned length, and move to an adjacent page only when the request has rows remaining. Returned chunks now preserve their half-open bounds at row zero and across page boundaries.
2026-07-10 06:54:20 -07:00
Mitchell Hashimoto
73ac36fa56 terminal/search: discard destroyed screen state
Search selection could run after the terminal removed a screen but before the next refresh reconciled the cached searchers. Reloading that stale ScreenSearch dereferenced freed PageList nodes, while normal cleanup also tried to untrack pins from the destroyed list.

Reconcile under the terminal lock before selecting, track ScreenSet generations so allocator address reuse cannot hide replacements, and release stale search buffers without touching pins already freed with the screen. Cleanup now takes the same lock when live pins must be untracked.
2026-07-10 06:54:20 -07:00
Mitchell Hashimoto
86e1f7b8b5 terminal/search: reject replaced result pages
History pruning only compared cached serials with page_serial_min. Replacing a historical node through compaction left its old serial above that cutoff, so selecting the cached match attempted to track a destroyed node and hit the PageList validity assertion.

Validate every flattened chunk by finding the live node and matching its serial without dereferencing stale pointers. Remove only the invalid result and adjust any later selection index so unrelated older results remain available.
2026-07-10 06:47:10 -07:00
Mitchell Hashimoto
5d8eb78b73 terminal/search: reset pins while feeding
PageListSearch.feed changed only the node of its tracked progress pin. If the preceding history page had fewer rows or columns after a split, the retained bottom-right coordinates fell outside that page and the next PageList integrity check panicked.

Reset both coordinates to the new node's actual bottom-right cell whenever feed advances. The progress pin now remains valid across heterogeneous page sizes.
2026-07-10 06:47:10 -07:00
Mitchell Hashimoto
d239f98056 terminal/search: drop pruned selections
Partial history erasure can remove a page without marking tracked pins as garbage because they move coherently to the next page. ScreenSearch therefore retained a selection whose cached history result was subsequently removed by pruneHistory. Selecting again indexed an empty history result list and panicked.

Clear the tracked selection when its combined result index falls within the history suffix being pruned. Retained active and newer history selections keep their existing indices.
2026-07-10 06:47:10 -07:00
Mitchell Hashimoto
cfce1cd56c terminal: duplicate hyperlinks before replacement
startHyperlink accepts borrowed URI and ID slices. When those slices came from the current cursor hyperlink, startHyperlinkOnce ended and freed that hyperlink before duplicating the replacement, then dereferenced the released URI and segfaulted.

Duplicate the new hyperlink before ending the prior one. Aliased inputs remain valid through the copy, and allocation failure leaves the existing cursor hyperlink intact.
2026-07-10 06:47:10 -07:00
Mitchell Hashimoto
dace6d1c6d terminal: handle aliased title updates
setTitle can receive the slice returned by getTitle. Clearing the list retained its allocation, so appending that same slice used memcpy with aliased source and destination ranges and panicked in runtime-safe builds.

Resize the list within its reserved capacity and copy the value forward before writing the sentinel. This supports the complete current value and its subslices without weakening allocation-failure atomicity.
2026-07-10 06:47:10 -07:00