refactor(terminal): wire kitty graphics and drop PageList workarounds

Integration pass over the terminal core (Codex/gpt-6-astra, reviewed):
- Screen now owns the real kitty ImageStorage; new/reset/teardown,
  erase-display deletes and alternate-screen limit inheritance hook in
  where Zig does, and Terminal implements ExecTerminal so kitty APC
  commands execute against it. Exec tests use a real Terminal.
- The PageList gains the APIs Screen/Terminal needed (pin iterators,
  cross-page row/partial-row clone, string allocator probing, dedup
  before allocating hyperlinks, Top viewport fixup after erase, append_
  grapheme no longer marking rows dirty), and the Screen/Terminal
  workarounds for their absence are removed so the code mirrors Zig.

Public signatures are unchanged; downstream crates build as before.

Test Plan:
- cargo test -p ghostty-terminal (1252 passed, 0 ignored)
- cargo clippy -p ghostty-terminal --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-11 22:57:35 +02:00
1 parent 69d823cbc7
commit 08452ac84f
12 files changed
+708 -476

No files matched your search

+65 -2
View File
@@ -845,7 +845,6 @@ impl Page {
self.grapheme_allocations.insert(i, a);
self.cells[i].set_content_tag(ContentTag::CodepointGrapheme);
self.rows[y].set_grapheme(true);
self.rows[y].set_dirty(true);
Ok(())
}
/// Append one additional codepoint, retaining allocation chunk semantics.
@@ -873,7 +872,6 @@ impl Page {
}
self.grapheme_data[range.end] = cp;
range.end += 1;
self.rows[y].set_dirty(true);
Ok(())
}
/// Release a cell's additional codepoints.
@@ -930,6 +928,12 @@ impl Page {
link: Hyperlink,
preferred: Option<u16>,
) -> Result<u16, Error> {
// Existing live links already own their strings. Retain them before
// attempting another allocation on a potentially full page.
if let Some(id) = self.hyperlink_set.lookup(&link) {
self.hyperlink_set.r#use(id).map_err(|_| Error::InvalidId)?;
return Ok(id);
}
let allocations = self.allocate_hyperlink_strings(&link)?;
let result = if let Some(id) = preferred {
self.hyperlink_set
@@ -1398,6 +1402,31 @@ impl Page {
self.rows[dst_y] = metadata;
Ok(())
}
/// Clone columns within a page with the same row metadata rules as a cross-page copy.
pub(crate) fn clone_partial_row_within(
&mut self,
src_y: usize,
dst_y: usize,
left: usize,
len: usize,
) -> Result<(), Error> {
let source_row = *self.get_row(src_y).ok_or(Error::InvalidCoordinate)?;
let original_row = *self.get_row(dst_y).ok_or(Error::InvalidCoordinate)?;
self.clone_cells_within(src_y, dst_y, left, left, len)?;
let managed = self.rows[dst_y];
let mut metadata = source_row;
metadata.set_cells(original_row.cells());
if len < usize::from(self.size.cols) {
metadata.set_wrap(original_row.wrap());
metadata.set_wrap_continuation(original_row.wrap_continuation());
metadata.set_grapheme(managed.grapheme());
metadata.set_styled(managed.styled());
metadata.set_hyperlink(managed.hyperlink());
metadata.set_dirty(source_row.dirty() || original_row.dirty());
}
self.rows[dst_y] = metadata;
Ok(())
}
/// Clone a range within the same page, including overlapping ranges.
pub fn clone_cells_within(
&mut self,
@@ -2383,4 +2412,38 @@ mod tests {
let q = clone_exact(&p, 1);
assert_eq!(q.hyperlink_count(), 50);
}
#[test]
fn grapheme_append_preserves_row_dirty_state() {
let mut p = page();
p.cells[0] = Cell::init('a' as u32);
p.rows[0].set_dirty(false);
for _ in 0..6 {
p.append_grapheme(0, 0, 0x301).unwrap();
assert!(!p.rows[0].dirty());
}
p.rows[0].set_dirty(true);
p.append_grapheme(0, 0, 0x302).unwrap();
assert!(p.rows[0].dirty());
p.assert_integrity();
}
#[test]
fn hyperlink_dedup_succeeds_with_full_string_allocator() {
let mut p = Page::init(Capacity {
cols: 2,
rows: 1,
string_bytes: 32,
..Capacity::default()
})
.unwrap();
let link = Hyperlink {
uri: vec![b'a'; Page::layout(p.capacity).string_alloc_layout.bitmap_count * 64 * 32],
id: HyperlinkId::Implicit(1),
};
let id = p.insert_hyperlink(link.clone()).unwrap();
assert!(!p.can_allocate_string(32));
assert_eq!(p.insert_hyperlink(link).unwrap(), id);
assert_eq!(p.hyperlink_set.ref_count(id), 2);
assert!(!p.can_allocate_string(32));
}
}