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:
1 parent
88cc13871c
commit
677683b740
12 files changed
+708
-476
No files matched your search
@@ -530,10 +530,11 @@ impl PageList {
|
||||
.filter(|s| s.generation == h.generation)?
|
||||
.pin
|
||||
}
|
||||
/// Update a tracked location to another valid cell.
|
||||
/// Update a tracked location. The node must be live; row bounds may be
|
||||
/// temporarily exceeded while a caller repairs cursor state after splitting.
|
||||
pub fn set_tracked_pin(&mut self, h: TrackedPin, p: Pin) -> Result<(), Error> {
|
||||
if !self.pin_is_valid(p) {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
if self.node(p.node).is_none() {
|
||||
return Err(Error::InvalidId);
|
||||
}
|
||||
let slot = self
|
||||
.pins
|
||||
@@ -813,12 +814,12 @@ impl PageList {
|
||||
}
|
||||
}
|
||||
fn fixup_viewport(&mut self) {
|
||||
if self.viewport == Viewport::Pin
|
||||
&& self
|
||||
.viewport_pin
|
||||
.and_then(|h| self.tracked_pin(h))
|
||||
.is_some_and(|p| self.pin_is_active(p))
|
||||
{
|
||||
let pin = match self.viewport {
|
||||
Viewport::Active => return,
|
||||
Viewport::Top => self.get_top_left(Tag::Screen),
|
||||
Viewport::Pin => self.viewport_pin.and_then(|h| self.tracked_pin(h)),
|
||||
};
|
||||
if pin.is_some_and(|p| self.pin_is_active(p)) {
|
||||
self.viewport = Viewport::Active;
|
||||
}
|
||||
}
|
||||
@@ -1054,6 +1055,10 @@ impl PageList {
|
||||
}
|
||||
/// Remove an inclusive row range, regrowing erased active rows.
|
||||
pub fn erase_rows(&mut self, start: Point, end: Option<Point>) -> Result<(), Error> {
|
||||
// An empty history produces no chunks in the reference iterator.
|
||||
if start.tag() == Tag::History && self.active_offset() == 0 && end.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let a = self
|
||||
.pin(start)
|
||||
.and_then(|p| self.screen_row(p))
|
||||
@@ -1085,9 +1090,7 @@ impl PageList {
|
||||
.store(cached - (b - a), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
if self.viewport == Viewport::Top && self.active_offset() == 0 {
|
||||
self.viewport = Viewport::Active;
|
||||
}
|
||||
self.fixup_viewport();
|
||||
Ok(())
|
||||
}
|
||||
/// Remove one row, shifting following rows without changing page dimensions.
|
||||
@@ -1544,6 +1547,79 @@ impl Pin {
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
impl Pin {
|
||||
/// Iterate cells toward an inclusive limit row. The limit column is ignored.
|
||||
pub fn cell_iterator(
|
||||
self,
|
||||
list: &PageList,
|
||||
direction: Direction,
|
||||
limit: Option<Pin>,
|
||||
) -> CellIterator<'_> {
|
||||
let (front, back) = match direction {
|
||||
Direction::RightDown => (
|
||||
Some(self),
|
||||
limit
|
||||
.or_else(|| list.get_bottom_right(Tag::Screen))
|
||||
.map(|p| Pin {
|
||||
x: list.cols - 1,
|
||||
..p
|
||||
}),
|
||||
),
|
||||
Direction::LeftUp => (
|
||||
limit
|
||||
.or_else(|| list.get_top_left(Tag::Screen))
|
||||
.map(|p| Pin { x: 0, ..p }),
|
||||
Some(self),
|
||||
),
|
||||
};
|
||||
CellIterator {
|
||||
list,
|
||||
front,
|
||||
back,
|
||||
direction,
|
||||
}
|
||||
}
|
||||
/// Iterate rows directly from a pin, with an inclusive limit row.
|
||||
pub fn row_iterator(
|
||||
self,
|
||||
list: &PageList,
|
||||
direction: Direction,
|
||||
limit: Option<Pin>,
|
||||
) -> RowIterator<'_> {
|
||||
let cells = self.cell_iterator(list, direction, limit);
|
||||
RowIterator {
|
||||
list,
|
||||
front: cells.front.map(|p| Pin { x: 0, ..p }),
|
||||
back: cells.back.map(|p| Pin { x: 0, ..p }),
|
||||
direction,
|
||||
}
|
||||
}
|
||||
/// Iterate maximal page chunks directly from a pin.
|
||||
pub fn page_iterator(
|
||||
self,
|
||||
list: &PageList,
|
||||
direction: Direction,
|
||||
limit: Option<Pin>,
|
||||
) -> PageIterator<'_> {
|
||||
PageIterator {
|
||||
rows: self.row_iterator(list, direction, limit),
|
||||
}
|
||||
}
|
||||
/// Iterate prompt starts directly from a pin, with an inclusive limit row.
|
||||
pub fn prompt_iterator(
|
||||
self,
|
||||
list: &PageList,
|
||||
direction: Direction,
|
||||
limit: Option<Pin>,
|
||||
) -> PromptIterator<'_> {
|
||||
PromptIterator {
|
||||
list,
|
||||
current: Some(self),
|
||||
limit,
|
||||
direction,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A double-ended cell iterator yielding stable untracked locations.
|
||||
pub struct CellIterator<'a> {
|
||||
list: &'a PageList,
|
||||
@@ -2350,7 +2426,9 @@ impl PageList {
|
||||
})?;
|
||||
self.resize_cols(cols, opts.cursor)?;
|
||||
}
|
||||
self.fixup_viewport();
|
||||
if self.viewport == Viewport::Pin {
|
||||
self.fixup_viewport();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn resize_without_reflow(&mut self, opts: Resize) -> Result<(), Error> {
|
||||
@@ -2400,12 +2478,52 @@ impl PageList {
|
||||
while self.total_rows < usize::from(rows) {
|
||||
self.grow()?;
|
||||
}
|
||||
self.fixup_viewport();
|
||||
if self.viewport == Viewport::Pin {
|
||||
self.fixup_viewport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Clone a complete row between two page locations, ignoring columns.
|
||||
pub fn clone_row_between(&mut self, src: Pin, dst: Pin) -> Result<(), Error> {
|
||||
if src.node == dst.node {
|
||||
let cols = usize::from(self.node(src.node).ok_or(Error::InvalidId)?.data.size.cols);
|
||||
return self.clone_partial_row_between(src, dst, 0, cols);
|
||||
}
|
||||
self.copy_row(src.node, src.y, dst.node, dst.y)
|
||||
}
|
||||
|
||||
/// Clone columns `[left, right)` between rows without a temporary row copy.
|
||||
pub fn clone_partial_row_between(
|
||||
&mut self,
|
||||
src: Pin,
|
||||
dst: Pin,
|
||||
left: usize,
|
||||
right: usize,
|
||||
) -> Result<(), Error> {
|
||||
let len = right.checked_sub(left).ok_or(Error::InvalidCoordinate)?;
|
||||
if src.node == dst.node {
|
||||
let page = &mut self.node_mut(src.node).ok_or(Error::InvalidId)?.data;
|
||||
return page.clone_partial_row_within(
|
||||
usize::from(src.y),
|
||||
usize::from(dst.y),
|
||||
left,
|
||||
len,
|
||||
);
|
||||
}
|
||||
let (source, dest) = self.pages_for_copy(src.node, dst.node)?;
|
||||
dest.clone_partial_row_from(
|
||||
source,
|
||||
usize::from(src.y),
|
||||
usize::from(dst.y),
|
||||
left,
|
||||
left,
|
||||
len,
|
||||
)
|
||||
}
|
||||
|
||||
/// Borrow distinct source and destination pages without snapshots or unsafe
|
||||
/// pointers. Their slab slots remain stable for the duration of the copy.
|
||||
fn copy_row(&mut self, src: NodeId, src_y: u16, dst: NodeId, dst_y: u16) -> Result<(), Error> {
|
||||
@@ -2413,7 +2531,9 @@ impl PageList {
|
||||
dest.clone_row_from(source, usize::from(src_y), usize::from(dst_y))
|
||||
}
|
||||
fn pages_for_copy(&mut self, src: NodeId, dst: NodeId) -> Result<(&Page, &mut Page), Error> {
|
||||
assert_ne!(src.index, dst.index);
|
||||
if self.node(src).is_none() || self.node(dst).is_none() || src.index == dst.index {
|
||||
return Err(Error::InvalidId);
|
||||
}
|
||||
let (source, dest) = if src.index < dst.index {
|
||||
let (left, right) = self.nodes.split_at_mut(dst.index);
|
||||
(left[src.index].as_ref(), right[0].as_mut())
|
||||
@@ -7449,4 +7569,91 @@ mod tests {
|
||||
assert_eq!(s.viewport_row_offset(), 0);
|
||||
finish(&s);
|
||||
}
|
||||
#[test]
|
||||
fn pin_iterators_use_inclusive_rows_across_pages() {
|
||||
let mut list = PageList::init(4, 4, None).unwrap();
|
||||
list.split(list.pin(active_point(0, 2)).unwrap()).unwrap();
|
||||
let start = list.pin(active_point(2, 1)).unwrap();
|
||||
let limit = list.pin(active_point(1, 2)).unwrap();
|
||||
let cells: Vec<_> = start
|
||||
.cell_iterator(&list, Direction::RightDown, Some(limit))
|
||||
.collect();
|
||||
assert_eq!(cells.len(), 6);
|
||||
assert_eq!(cells[0], start);
|
||||
assert_eq!(cells[5].x, 3);
|
||||
let reverse: Vec<_> = limit
|
||||
.cell_iterator(&list, Direction::LeftUp, Some(start))
|
||||
.collect();
|
||||
assert_eq!(reverse.len(), 6);
|
||||
assert_eq!(reverse[0], limit);
|
||||
assert_eq!(reverse[5].x, 0);
|
||||
for (start, limit, direction) in [
|
||||
(start, limit, Direction::RightDown),
|
||||
(limit, start, Direction::LeftUp),
|
||||
] {
|
||||
assert_eq!(start.row_iterator(&list, direction, Some(limit)).count(), 2);
|
||||
let pages: Vec<_> = start.page_iterator(&list, direction, Some(limit)).collect();
|
||||
assert_eq!(pages.len(), 2);
|
||||
assert!(pages.iter().all(|c| c.end - c.start == 1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_copy_between_pages_preserves_managed_values_and_metadata() {
|
||||
let mut list = PageList::init(4, 4, None).unwrap();
|
||||
list.split(list.pin(active_point(0, 2)).unwrap()).unwrap();
|
||||
let src = list.pin(active_point(0, 0)).unwrap();
|
||||
let dst = list.pin(active_point(0, 2)).unwrap();
|
||||
let page = &mut list.node_mut(src.node).unwrap().data;
|
||||
page.cells[1] = Cell::init('a' as u32);
|
||||
page.append_grapheme(1, 0, 0x301).unwrap();
|
||||
let id = page
|
||||
.insert_hyperlink(crate::hyperlink::Hyperlink {
|
||||
uri: b"https://example.com".to_vec(),
|
||||
id: crate::hyperlink::HyperlinkId::Implicit(1),
|
||||
})
|
||||
.unwrap();
|
||||
page.set_hyperlink(1, 0, id).unwrap();
|
||||
page.rows[0].set_kitty_virtual_placeholder(true);
|
||||
page.rows[0].set_dirty(false);
|
||||
let page = &mut list.node_mut(dst.node).unwrap().data;
|
||||
page.cells[0] = Cell::init('z' as u32);
|
||||
page.rows[0].set_wrap(true);
|
||||
page.rows[0].set_dirty(true);
|
||||
list.clone_partial_row_between(src, dst, 1, 2).unwrap();
|
||||
let page = &list.node(dst.node).unwrap().data;
|
||||
assert_eq!(page.get_cells(0).unwrap()[0].codepoint(), 'z' as u32);
|
||||
assert!(page.rows[0].wrap());
|
||||
assert!(page.rows[0].dirty());
|
||||
assert!(page.rows[0].kitty_virtual_placeholder());
|
||||
assert_eq!(
|
||||
page.lookup_grapheme(page.cell_index(1, 0).unwrap()),
|
||||
Some(&[0x301][..])
|
||||
);
|
||||
assert!(
|
||||
page.lookup_hyperlink(page.cell_index(1, 0).unwrap())
|
||||
.is_some()
|
||||
);
|
||||
list.clone_row_between(src, dst).unwrap();
|
||||
let page = &list.node(dst.node).unwrap().data;
|
||||
assert_eq!(page.get_cells(0).unwrap()[0].codepoint(), 0);
|
||||
assert!(!page.rows[0].wrap());
|
||||
assert!(!page.rows[0].dirty());
|
||||
list.assert_integrity();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erase_empty_history_and_top_viewport() {
|
||||
let mut list = PageList::init(4, 4, None).unwrap();
|
||||
list.erase_rows(Point::History(Coordinate::default()), None)
|
||||
.unwrap();
|
||||
assert_eq!(list.total_rows, 4);
|
||||
list.grow().unwrap();
|
||||
list.scroll(Scroll::Top);
|
||||
list.erase_rows(Point::History(Coordinate::default()), None)
|
||||
.unwrap();
|
||||
assert_eq!(list.total_rows, 4);
|
||||
assert_eq!(list.viewport, Viewport::Active);
|
||||
list.assert_integrity();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user