From 6e7d415bcfc09bc5ad293cf1db919e9d17c28034 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Fri, 11 Sep 2026 02:08:31 +0300 Subject: fix: keep terminal snapshots consistent while scrolling (#3928) refs #3900 --- src/server/headless/retained_surface.rs | 27 ++++++---------- src/server/headless/tests/mod.rs | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 17 deletions(-) (limited to 'src/server') diff --git a/src/server/headless/retained_surface.rs b/src/server/headless/retained_surface.rs index 259714a3..d0fb9bba 100644 --- a/src/server/headless/retained_surface.rs +++ b/src/server/headless/retained_surface.rs @@ -317,11 +317,10 @@ impl HeadlessServer { ) else { fallback!("runtime_missing"); }; - let revision_before = runtime.content_seq(); - if !revision_before.is_multiple_of(2) { - fallback!("unstable_content"); - } - let patch = match runtime.collect_dirty_patch(width, height) { + let Some(snapshot) = runtime.collect_dirty_patch_snapshot(width, height) else { + fallback!("terminal_snapshot"); + }; + let patch = match snapshot.patch { crate::pane::TerminalDirtyPatchOutcome::Clean => { crate::render_prof::event("retained_surface.pane_clean"); crate::pane::TerminalDirtyPatch { rows: Vec::new() } @@ -331,21 +330,15 @@ impl HeadlessServer { fallback!("terminal_patch"); } }; - let graphics_may_have_placements = - crate::kitty_graphics::is_enabled() && runtime.kitty_graphics_may_have_placements(); - let revision = runtime.content_seq(); - if revision != revision_before || !revision.is_multiple_of(2) { - fallback!("content_changed"); - } collected.push(CollectedPanePatch { pane_id: public_pane_id, patch, - content_revision: revision, - scroll_metrics: runtime.scroll_metrics(), - mouse_reporting: runtime.mouse_reporting_enabled(), - sgr_pixel_mouse: runtime.sgr_pixel_mouse_enabled(), - alternate_screen_active: runtime.alternate_screen_active(), - graphics_may_have_placements, + content_revision: snapshot.content_revision, + scroll_metrics: snapshot.scroll_metrics, + mouse_reporting: snapshot.mouse_reporting, + sgr_pixel_mouse: snapshot.sgr_pixel_mouse, + alternate_screen_active: snapshot.alternate_screen_active, + graphics_may_have_placements: snapshot.graphics_may_have_placements, }); } diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs index f124e52e..d666ae22 100644 --- a/src/server/headless/tests/mod.rs +++ b/src/server/headless/tests/mod.rs @@ -1000,6 +1000,62 @@ fn recv_pane_surface_patch( } } +#[tokio::test] +async fn retained_snapshot_survives_a_writer_waiting_for_the_terminal_core() { + let mut server = test_headless_server(); + let pane_id = install_shared_view_test_runtime(&mut server); + let (control, render) = connect_matching_test_shell(&mut server, 7); + let _ = control.recv().expect("snapshot"); + server.render_and_stream(); + let _ = recv_pane_surface(&render, "initial surface"); + + let (release, writer, revision) = { + let runtime = server + .app + .state + .runtime_for_pane_in_workspace(&server.app.terminal_runtimes, 0, pane_id) + .expect("runtime"); + runtime.test_process_pty_bytes(b"\rAAAA\x1b[?1003h"); + let revision = runtime.content_seq(); + let (release, writer) = + runtime.test_contend_during_dirty_collection(b"\rBBBB\x1b[?1003l".to_vec()); + (release, writer, revision) + }; + let retained = server.render_retained_pane_surface_and_stream(&HashSet::from([pane_id])); + release.send(()).expect("release waiting writer"); + let announced = writer.join().expect("writer completed"); + + assert!( + retained, + "a waiting writer must not invalidate the collected snapshot" + ); + assert!( + !announced, + "writer must wait before announcing a new revision" + ); + let patch = recv_pane_surface_patch(&render, "snapshot before waiting write"); + assert_eq!(patch.panes[0].content_revision, revision); + assert!(revision.is_multiple_of(2)); + assert!(patch.panes[0].mouse_reporting); + let surface = server.clients[&7] + .render_state + .last_pane_surface() + .expect("surface"); + assert!(frame_text(&surface.frame).contains("AAAA")); + assert!(!frame_text(&surface.frame).contains("BBBB")); + + assert!(server.render_retained_pane_surface_and_stream(&HashSet::from([pane_id]))); + let next = recv_pane_surface_patch(&render, "waiting write remains dirty"); + assert_eq!(next.panes[0].content_revision, revision + 2); + assert!(!next.panes[0].mouse_reporting); + let surface = server.clients[&7] + .render_state + .last_pane_surface() + .expect("next surface"); + assert!(frame_text(&surface.frame).contains("BBBB")); + shutdown_test_runtimes(&mut server); +} + #[tokio::test] async fn different_size_shells_receive_geometry_specific_patches_from_one_dirty_collection() { let mut server = test_headless_server(); -- cgit v1.2.3