aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-11 02:08:31 +0300
committerGitHub <noreply@github.com>2026-09-11 02:08:31 +0300
commit6e7d415bcfc09bc5ad293cf1db919e9d17c28034 (patch)
treee034d340d55006a91635a8ca29df75a14a2445c8 /src/server
parent58271459401fa461707c042f7f03867ddcfff3ea (diff)
fix: keep terminal snapshots consistent while scrolling (#3928)
refs #3900
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless/retained_surface.rs27
-rw-r--r--src/server/headless/tests/mod.rs56
2 files changed, 66 insertions, 17 deletions
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
@@ -1001,6 +1001,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();
let pane_id = install_shared_view_test_runtime(&mut server);