aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/clients.rs5
-rw-r--r--src/server/headless.rs13
-rw-r--r--src/server/headless/tests/pane_graphics.rs48
-rw-r--r--src/server/render_stream.rs56
4 files changed, 93 insertions, 29 deletions
diff --git a/src/server/clients.rs b/src/server/clients.rs
index 3782bd15..c6c617ec 100644
--- a/src/server/clients.rs
+++ b/src/server/clients.rs
@@ -135,9 +135,8 @@ impl ClientConnection {
}
}
- pub(crate) fn request_full_redraw(&mut self) {
- self.render_state.reset_baseline();
- self.graphics_surface_reset_pending = true;
+ pub(crate) fn request_repaint(&mut self) {
+ self.render_state.request_repaint();
self.pane_graphics_render_pending = false;
}
diff --git a/src/server/headless.rs b/src/server/headless.rs
index f330fbec..aaa3742a 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -971,7 +971,7 @@ impl HeadlessServer {
// rendering semantics. Force one fresh frame to every remaining client
// even if the next rendered buffer compares equal to its cached frame.
for client in self.clients.values_mut() {
- client.request_full_redraw();
+ client.request_repaint();
}
if !start_pending_agent_resumes {
self.app.pending_agent_resume_deadline = None;
@@ -984,7 +984,7 @@ impl HeadlessServer {
.start_pending_agent_resumes(self.app.pending_agent_resume_due(now))
{
for client in self.clients.values_mut() {
- client.request_full_redraw();
+ client.request_repaint();
}
}
}
@@ -2585,7 +2585,7 @@ impl HeadlessServer {
);
if let Some(client) = self.clients.get_mut(&client_id) {
if host_surface_redraw {
- client.request_full_redraw();
+ client.request_repaint();
client.defer_full_render();
} else {
// Ensure semantic clients receive one post-input frame even if the
@@ -2872,7 +2872,7 @@ impl HeadlessServer {
width_px: cell_width_px,
height_px: cell_height_px,
};
- render_state.reset_baseline();
+ render_state.request_repaint();
Some(terminal_id.clone())
} else {
None
@@ -2896,7 +2896,7 @@ impl HeadlessServer {
width_px: cell_width_px,
height_px: cell_height_px,
};
- render_state.reset_baseline();
+ render_state.request_repaint();
return true;
}
if let Some(client) = self.clients.get_mut(&client_id) {
@@ -7611,7 +7611,7 @@ next_tab = ""
}
#[test]
- fn outer_focus_gained_forces_terminal_ansi_full_redraw() {
+ fn outer_focus_gained_repaints_terminal_ansi_without_clearing() {
let mut server = test_headless_server();
let (client_tx, _client_control_rx, client_rx) = test_client_writer();
@@ -7644,6 +7644,7 @@ next_tab = ""
ServerMessage::Terminal(frame) => {
assert_eq!(frame.seq, 2);
assert!(frame.full);
+ assert!(!frame.bytes.windows(4).any(|bytes| bytes == b"\x1b[2J"));
}
other => panic!("expected terminal frame, got {other:?}"),
}
diff --git a/src/server/headless/tests/pane_graphics.rs b/src/server/headless/tests/pane_graphics.rs
index 627ffc78..1eba9816 100644
--- a/src/server/headless/tests/pane_graphics.rs
+++ b/src/server/headless/tests/pane_graphics.rs
@@ -86,6 +86,52 @@ fn stream_set_message(
}
#[tokio::test]
+async fn focus_repaint_preserves_uploaded_graphics() {
+ let (mut server, client_rx, pane_id) = retained_test_server(b"aaaa");
+ let (client_2_writer, _client_2_control_rx, client_2_rx) = test_client_writer();
+ server.clients.insert(
+ 2,
+ ClientConnection::new(
+ (80, 24),
+ crate::kitty_graphics::HostCellSize {
+ width_px: 10,
+ height_px: 20,
+ },
+ crate::terminal_theme::TerminalTheme::default(),
+ Some(false),
+ 0,
+ RenderEncoding::SemanticFrame,
+ Some(client_2_writer),
+ ),
+ );
+ set_graphics_layer(&mut server, pane_id, vec![1, 2, 3]);
+ let initial = enable_graphics_and_render(&mut server, &client_rx);
+ let initial_graphics = String::from_utf8_lossy(&initial.graphics);
+ assert!(initial_graphics.contains("a=t"));
+ assert!(initial_graphics.contains("a=p"));
+ let client_2_initial = read_server_frame(
+ client_2_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("second client initial frame"),
+ );
+ assert!(String::from_utf8_lossy(&client_2_initial.graphics).contains("a=t"));
+
+ assert!(server.handle_server_event(ServerEvent::ClientInput {
+ client_id: 2,
+ data: b"\x1b[I".to_vec(),
+ }));
+ assert_eq!(server.foreground_client_id, Some(2));
+ server.render_and_stream();
+
+ let focused = read_server_frame(
+ client_2_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("focus redraw"),
+ );
+ assert!(focused.graphics.is_empty());
+}
+
+#[tokio::test]
async fn retained_update_sends_only_graphics_message() {
let (mut server, client_rx, pane_id) = retained_test_server(b"aaaa");
let baseline = enable_graphics_and_render(&mut server, &client_rx);
@@ -148,7 +194,7 @@ async fn retained_update_does_not_downgrade_pending_full_render() {
let _ = enable_graphics_and_render(&mut server, &client_rx);
fill_render_lane(&server);
let client = server.clients.get_mut(&1).unwrap();
- client.request_full_redraw();
+ client.request_repaint();
server.render_and_stream();
assert_eq!(
server.clients.get(&1).unwrap().deferred_render(),
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index 8858bb6b..f1f6c428 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -14,7 +14,11 @@ pub(crate) enum ClientRenderState {
/// Semantic clients compare full frame data and skip identical frames.
Semantic { last_frame: Option<FrameData> },
/// Terminal-ANSI clients keep a terminal diff encoder and sequence number.
- TerminalAnsi { blit_encoder: BlitEncoder, seq: u64 },
+ TerminalAnsi {
+ blit_encoder: BlitEncoder,
+ seq: u64,
+ repaint_pending: bool,
+ },
}
impl ClientRenderState {
@@ -24,6 +28,7 @@ impl ClientRenderState {
RenderEncoding::TerminalAnsi => Self::TerminalAnsi {
blit_encoder: BlitEncoder::new(),
seq: 0,
+ repaint_pending: false,
},
}
}
@@ -31,7 +36,23 @@ impl ClientRenderState {
pub(crate) fn reset_baseline(&mut self) {
match self {
Self::Semantic { last_frame } => *last_frame = None,
- Self::TerminalAnsi { blit_encoder, .. } => *blit_encoder = BlitEncoder::new(),
+ Self::TerminalAnsi {
+ blit_encoder,
+ repaint_pending,
+ ..
+ } => {
+ *blit_encoder = BlitEncoder::new();
+ *repaint_pending = false;
+ }
+ }
+ }
+
+ pub(crate) fn request_repaint(&mut self) {
+ match self {
+ Self::Semantic { last_frame } => *last_frame = None,
+ Self::TerminalAnsi {
+ repaint_pending, ..
+ } => *repaint_pending = true,
}
}
@@ -53,12 +74,16 @@ impl ClientRenderState {
message: ServerMessage::Frame(frame),
})
}
- Self::TerminalAnsi { blit_encoder, seq } => {
- if blit_encoder.is_current(&frame) {
+ Self::TerminalAnsi {
+ blit_encoder,
+ seq,
+ repaint_pending,
+ } => {
+ if !*repaint_pending && blit_encoder.is_current(&frame) {
crate::render_prof::event("prepare_frame.ansi.skip_current");
return None;
}
- let mut encoded = blit_encoder.encode(&frame, false);
+ let mut encoded = blit_encoder.encode(&frame, *repaint_pending);
crate::render_prof::event("prepare_frame.ansi.changed");
crate::render_prof::counter("prepare_frame.ansi.bytes", encoded.bytes.len() as u64);
if encoded.full {
@@ -102,7 +127,11 @@ impl ClientRenderState {
},
) => *last_frame = Some(frame),
(
- Self::TerminalAnsi { blit_encoder, seq },
+ Self::TerminalAnsi {
+ blit_encoder,
+ seq,
+ repaint_pending,
+ },
PreparedRender::TerminalAnsi {
frame,
encoded: Some(encoded),
@@ -111,6 +140,7 @@ impl ClientRenderState {
) => {
blit_encoder.commit(frame, encoded);
*seq += 1;
+ *repaint_pending = false;
}
_ => {}
}
@@ -125,30 +155,18 @@ impl ClientRenderState {
}
}
-const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
-
fn insert_graphics_before_sync_end(encoded: &mut Vec<u8>, graphics: &[u8]) {
if graphics.is_empty() {
return;
}
- if let Some(sync_end) = rfind_subslice(encoded, SYNC_OUTPUT_END) {
+ if let Some(sync_end) = crate::protocol::render_ansi::final_sync_output_end(encoded) {
encoded.splice(sync_end..sync_end, graphics.iter().copied());
} else {
encoded.extend_from_slice(graphics);
}
}
-fn rfind_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
- if needle.is_empty() || needle.len() > haystack.len() {
- return None;
- }
-
- haystack
- .windows(needle.len())
- .rposition(|window| window == needle)
-}
-
/// A prepared client render message plus any baseline state needed after send.
pub(crate) enum PreparedRender {
Semantic {