feat(terminal): bring PageList to full parity with Ghostty

Second pass over the page memory model (done by Codex/gpt-6-astra in an
isolated worktree, reviewed and merged here):
- Resize/reflow now port Zig's incremental ReflowCursor algorithm with
  page backfill and the same capacity growth/split sequence, instead of
  rebuilding a snapshot, so page boundaries, pruning and tracked-pin
  remapping match Ghostty.
- Point<->pin conversion for the active area starts at the list tail and
  the viewport offset is cached, so hot-path operations no longer walk
  the whole scrollback from the head.
- RefCountedSet reproduces Zig's observable ID reuse and error points.
- All 202 PageList tests are ported (plus 13 extra regressions).

With this, the five Screen/Terminal tests previously ignored as blocked
on PageList behaviour pass and are re-enabled.

Test Plan:
- cargo test -p ghostty-terminal (913 passed, 0 ignored)
- cargo clippy -p ghostty-terminal --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-11 22:41:14 +02:00
1 parent d24b40cf2b
commit 83c10debe6
9 files changed
+4613 -1770

No files matched your search

+45
View File
@@ -119,6 +119,51 @@ pub struct Fg<'a> {
pub bold: Option<BoldColor>,
}
impl Style {
/// Ghostty's packed-style hash: fold the exact 128-bit representation and
/// apply Zig's 64-bit integer hash. This determines page-local probe order.
pub(crate) fn zig_hash(&self) -> u64 {
const MULTIPLIER: u64 = 0xbea225f9eb34556d;
let mut packed = 0u128;
for (index, color) in [self.fg_color, self.bg_color, self.underline_color]
.into_iter()
.enumerate()
{
let (tag, data) = match color {
Color::None => (0, 0),
Color::Palette(value) => (1, u32::from(value)),
Color::Rgb(rgb) => (
2,
u32::from(rgb.r) | u32::from(rgb.g) << 8 | u32::from(rgb.b) << 16,
),
};
packed |= (tag as u128) << (index * 8);
packed |= u128::from(data) << (24 + index * 24);
}
let flags = self.flags;
let mut bits = 0u16;
for (index, value) in [
flags.bold,
flags.italic,
flags.faint,
flags.blink,
flags.inverse,
flags.invisible,
flags.strikethrough,
flags.overline,
]
.into_iter()
.enumerate()
{
bits |= u16::from(value) << index;
}
bits |= (flags.underline as u16) << 8;
packed |= u128::from(bits) << 96;
let mut hash = packed as u64 ^ (packed >> 64) as u64;
hash = (hash ^ (hash >> 32)).wrapping_mul(MULTIPLIER);
hash = (hash ^ (hash >> 29)).wrapping_mul(MULTIPLIER);
hash = (hash ^ (hash >> 32)).wrapping_mul(MULTIPLIER);
hash ^ (hash >> 29)
}
/// True for default styling.
pub fn is_default(self) -> bool {
self == Self::default()