aboutsummaryrefslogtreecommitdiff
path: root/src/client/blit.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-08 22:19:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-08 22:20:04 +0300
commit793f127e54dd5f2cfb78922470b7efcb74a1fa5e (patch)
treedf9a925c7d4dc4be64ffd77cf5dfe3e5abb273a0 /src/client/blit.rs
parentbb0dad86f81768c8b7d5894acf7cd53bd05d302b (diff)
feat(remote): add network-efficient thin client transport
Add SSH stdio remote attach, negotiate terminal-ANSI render encoding, move blit encoding server-side for remote clients, and split control/render writer delivery for safer backpressure. Fixes #76
Diffstat (limited to 'src/client/blit.rs')
-rw-r--r--src/client/blit.rs66
1 files changed, 51 insertions, 15 deletions
diff --git a/src/client/blit.rs b/src/client/blit.rs
index 3d562563..a206f97e 100644
--- a/src/client/blit.rs
+++ b/src/client/blit.rs
@@ -24,12 +24,62 @@
//! and minimize cursor movement.
use std::cmp;
-use std::io::{self, Write};
+use std::io::Write;
use unicode_width::UnicodeWidthStr;
use crate::server::protocol::{CellData, FrameData};
+/// Bytes produced by a [`BlitEncoder`] for one terminal frame.
+pub(crate) struct EncodedBlit {
+ /// Terminal escape bytes ready to write to the host terminal.
+ pub(crate) bytes: Vec<u8>,
+ /// Whether this frame was encoded as a full redraw.
+ pub(crate) full: bool,
+ next_last_visible_cursor: Option<(u16, u16)>,
+}
+
+/// Stateful encoder that diffs semantic frames into terminal ANSI bytes.
+#[derive(Default)]
+pub(crate) struct BlitEncoder {
+ last_frame: Option<FrameData>,
+ last_visible_cursor: Option<(u16, u16)>,
+}
+
+impl BlitEncoder {
+ pub(crate) fn new() -> Self {
+ Self::default()
+ }
+
+ pub(crate) fn encode(&self, frame: &FrameData, force_full: bool) -> EncodedBlit {
+ let prev = if force_full {
+ None
+ } else {
+ self.last_frame.as_ref()
+ };
+ let full = force_full
+ || prev.is_none()
+ || prev.is_some_and(|p| p.width != frame.width || p.height != frame.height);
+ let mut bytes = Vec::new();
+ let mut next_last_visible_cursor = self.last_visible_cursor;
+ blit_frame_to_with_cursor_memory(&mut bytes, frame, prev, &mut next_last_visible_cursor);
+ EncodedBlit {
+ bytes,
+ full,
+ next_last_visible_cursor,
+ }
+ }
+
+ pub(crate) fn commit(&mut self, frame: FrameData, encoded: EncodedBlit) {
+ self.last_visible_cursor = encoded.next_last_visible_cursor;
+ self.last_frame = Some(frame);
+ }
+
+ pub(crate) fn is_current(&self, frame: &FrameData) -> bool {
+ self.last_frame.as_ref() == Some(frame)
+ }
+}
+
// ---------------------------------------------------------------------------
// Color → escape sequence
// ---------------------------------------------------------------------------
@@ -190,20 +240,6 @@ fn cells_equal(a: &CellData, b: &CellData) -> bool {
// Blitting
// ---------------------------------------------------------------------------
-/// Blits a frame to the terminal, diffing against the previous frame.
-///
-/// On the first frame (no previous), does a full redraw.
-/// On subsequent frames, only writes cells that changed.
-/// After writing cells, positions the cursor.
-pub fn blit_frame_with_cursor_memory(
- frame: &FrameData,
- prev: Option<&FrameData>,
- last_visible_cursor: &mut Option<(u16, u16)>,
-) {
- let mut stdout = io::stdout();
- blit_frame_to_with_cursor_memory(&mut stdout, frame, prev, last_visible_cursor);
-}
-
/// Blits a frame to a writer, diffing against the previous frame.
#[cfg(test)]
fn blit_frame_to(writer: impl Write, frame: &FrameData, prev: Option<&FrameData>) {