aboutsummaryrefslogtreecommitdiff
path: root/src/protocol
diff options
context:
space:
mode:
Diffstat (limited to 'src/protocol')
-rw-r--r--src/protocol/endpoint.rs8
-rw-r--r--src/protocol/mod.rs1
-rw-r--r--src/protocol/surface_reuse.rs127
-rw-r--r--src/protocol/wire.rs17
4 files changed, 152 insertions, 1 deletions
diff --git a/src/protocol/endpoint.rs b/src/protocol/endpoint.rs
index 640e594e..2f7c8338 100644
--- a/src/protocol/endpoint.rs
+++ b/src/protocol/endpoint.rs
@@ -46,6 +46,9 @@ pub struct EndpointClientHello {
pub mouse_capture: bool,
#[serde(default = "default_true")]
pub surface_active: bool,
+ /// Accept the optional cell-retaining surface encoding on this connection.
+ #[serde(default)]
+ pub surface_reuse: bool,
#[serde(default)]
pub snapshot_codecs: Vec<String>,
#[serde(default)]
@@ -139,6 +142,7 @@ impl EndpointServerWelcome {
blob_codec: BLOB_CODEC_V1.into(),
methods,
capabilities: vec![
+ super::surface_reuse::CAPABILITY.into(),
SURFACE_INTEREST_CAPABILITY.into(),
PRESENTATION_EFFECTS_FENCE_CAPABILITY.into(),
HEALTH_CHECK_CAPABILITY.into(),
@@ -181,6 +185,7 @@ mod tests {
endpoint_keybindings: false,
mouse_capture: true,
surface_active: true,
+ surface_reuse: false,
snapshot_codecs: vec![SNAPSHOT_CODEC_V1.into()],
surface_codecs: vec![SURFACE_CODEC_V1.into()],
input_codecs: vec![INPUT_CODEC_V1.into()],
@@ -327,8 +332,10 @@ mod tests {
fn legacy_hello_defaults_to_an_active_surface() {
let mut value = serde_json::to_value(hello()).unwrap();
value.as_object_mut().unwrap().remove("surface_active");
+ value.as_object_mut().unwrap().remove("surface_reuse");
let decoded: EndpointClientHello = serde_json::from_value(value).unwrap();
assert!(decoded.surface_active);
+ assert!(!decoded.surface_reuse);
}
#[test]
@@ -337,6 +344,7 @@ mod tests {
assert_eq!(
welcome.capabilities,
vec![
+ super::super::surface_reuse::CAPABILITY.to_string(),
SURFACE_INTEREST_CAPABILITY.to_string(),
PRESENTATION_EFFECTS_FENCE_CAPABILITY.to_string(),
HEALTH_CHECK_CAPABILITY.to_string(),
diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs
index 7ee6e8de..4a8766b6 100644
--- a/src/protocol/mod.rs
+++ b/src/protocol/mod.rs
@@ -2,6 +2,7 @@
pub mod endpoint;
pub(crate) mod render_ansi;
+pub(crate) mod surface_reuse;
mod wire;
pub use wire::*;
diff --git a/src/protocol/surface_reuse.rs b/src/protocol/surface_reuse.rs
new file mode 100644
index 00000000..bba85824
--- /dev/null
+++ b/src/protocol/surface_reuse.rs
@@ -0,0 +1,127 @@
+//! Optional endpoint encoding that retains unchanged terminal cells across projections.
+
+use super::{CellData, PaneSurfaceFrame, ServerMessage};
+use serde::{Deserialize, Serialize};
+
+pub(crate) const CAPABILITY: &str = "surface_reuse";
+pub(crate) const MESSAGE_KIND: &str = "endpoint.surface-reuse.v1";
+
+#[derive(Serialize, Deserialize)]
+struct SurfaceReuse<S> {
+ base_surface_revision: u64,
+ surface: S,
+}
+
+pub(crate) fn message(
+ base_surface_revision: u64,
+ surface: &mut PaneSurfaceFrame,
+) -> serde_json::Result<Option<ServerMessage>> {
+ let cells = std::mem::take(&mut surface.frame.cells);
+ let data = serde_json::to_string(&SurfaceReuse {
+ base_surface_revision,
+ surface: &*surface,
+ });
+ surface.frame.cells = cells;
+ let message = ServerMessage::EndpointControl {
+ kind: MESSAGE_KIND.into(),
+ data: data?,
+ };
+ // JSON can expand non-cell data (for example escaped hyperlink URLs). A
+ // failed compact encoding must fall back, not strand a newer snapshot.
+ let size = bincode::serde::encode_into_std_write(
+ &message,
+ &mut std::io::sink(),
+ bincode::config::standard(),
+ );
+ match size {
+ Ok(size) => Ok((size <= super::MAX_FRAME_SIZE).then_some(message)),
+ Err(error) => {
+ tracing::warn!(%error, "failed to size surface reuse");
+ Ok(None)
+ }
+ }
+}
+
+#[derive(Default)]
+struct CellBaseline {
+ boot_id: String,
+ projection_revision: u64,
+ surface_revision: u64,
+ width: u16,
+ height: u16,
+ cells: Vec<CellData>,
+}
+
+/// Connection-local decoding happens before activation and presentation filtering, so
+/// switching endpoints cannot discard a baseline needed by the next wire message.
+#[derive(Default)]
+pub(crate) struct Decoder {
+ baseline: Option<CellBaseline>,
+}
+
+impl Decoder {
+ pub(crate) fn decode(&mut self, message: ServerMessage) -> Result<ServerMessage, String> {
+ let message = match message {
+ ServerMessage::EndpointControl { kind, data } if kind == MESSAGE_KIND => {
+ let reuse: SurfaceReuse<PaneSurfaceFrame> = serde_json::from_str(&data)
+ .map_err(|error| format!("invalid surface reuse: {error}"))?;
+ let Some(base) = &mut self.baseline else {
+ return Err("surface reuse without a baseline".into());
+ };
+ let mut surface = reuse.surface;
+ if base.boot_id != surface.boot_id
+ || base.surface_revision != reuse.base_surface_revision
+ || surface.surface_revision != base.surface_revision.saturating_add(1)
+ || base.width != surface.frame.width
+ || base.height != surface.frame.height
+ || !surface.frame.cells.is_empty()
+ {
+ return Err("surface reuse does not match its baseline".into());
+ }
+ surface.frame.cells.clone_from(&base.cells);
+ base.projection_revision = surface.projection_revision;
+ base.surface_revision = surface.surface_revision;
+ return Ok(ServerMessage::PaneSurface(surface));
+ }
+ message => message,
+ };
+ match &message {
+ ServerMessage::PaneSurface(surface) => {
+ let base = self.baseline.get_or_insert_with(CellBaseline::default);
+ base.boot_id.clone_from(&surface.boot_id);
+ base.projection_revision = surface.projection_revision;
+ base.surface_revision = surface.surface_revision;
+ base.width = surface.frame.width;
+ base.height = surface.frame.height;
+ base.cells.clone_from(&surface.frame.cells);
+ }
+ ServerMessage::PaneSurfacePatch(patch) => {
+ if let Some(base) = &mut self.baseline {
+ if patch.boot_id != base.boot_id
+ || patch.projection_revision != base.projection_revision
+ || patch.base_surface_revision != base.surface_revision
+ || patch.surface_revision != base.surface_revision.saturating_add(1)
+ {
+ self.baseline = None;
+ } else {
+ for row in &patch.rows {
+ let start =
+ usize::from(row.y) * usize::from(base.width) + usize::from(row.x);
+ let end = start.saturating_add(row.cells.len());
+ if row.y >= base.height
+ || usize::from(row.x) + row.cells.len() > usize::from(base.width)
+ || end > base.cells.len()
+ {
+ return Err("surface patch exceeds the cell baseline".into());
+ }
+ base.cells[start..end].clone_from_slice(&row.cells);
+ }
+ base.surface_revision = patch.surface_revision;
+ }
+ }
+ }
+ _ => {}
+ }
+ Ok(message)
+ }
+}
diff --git a/src/protocol/wire.rs b/src/protocol/wire.rs
index 052c73a3..a29c6ca6 100644
--- a/src/protocol/wire.rs
+++ b/src/protocol/wire.rs
@@ -699,7 +699,7 @@ pub enum AttachScrollSource {
/// A single cell in a rendered frame, serialized independently from ratatui's
/// `Cell` type to keep the wire protocol stable.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CellData {
/// Grapheme cluster displayed in this cell (usually 1–2 chars).
pub symbol: String,
@@ -715,6 +715,21 @@ pub struct CellData {
pub hyperlink: Option<u32>,
}
+impl Clone for CellData {
+ fn clone(&self) -> Self {
+ Self {
+ symbol: self.symbol.clone(),
+ ..*self
+ }
+ }
+
+ fn clone_from(&mut self, source: &Self) {
+ let mut symbol = std::mem::take(&mut self.symbol);
+ symbol.clone_from(&source.symbol);
+ *self = Self { symbol, ..*source };
+ }
+}
+
impl CellData {
pub(crate) fn from_ratatui_cell(cell: &ratatui::buffer::Cell) -> Self {
Self {