From ef7a679c2b1ea5d064fbee155a55d527ce929be9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 20 Jun 2025 02:31:15 +0200 Subject: [PATCH] Fix binary buffer encoding to support large terminals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binary encoding was using 16-bit unsigned integers which failed when: - Terminal has many rows (>65k) - Cursor is above the viewport (negative relative position) Changes: - Upgrade to version 2 of the binary format - Use 32-bit integers for dimensions and positions - Use signed integers for viewport/cursor positions - Update header size from 16 to 32 bytes - Update documentation to reflect new format This fixes the issue where cursorY could be negative when the cursor is above the visible viewport (e.g., cursorY=0, viewportY=46 results in relative cursorY=-46). 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- web/snapshot-format.md | 39 ++++++++++++----------- web/src/client/utils/terminal-renderer.ts | 26 +++++++-------- web/src/terminal-manager.ts | 30 ++++++++--------- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/web/snapshot-format.md b/web/snapshot-format.md index f7e3bac7..d2140728 100644 --- a/web/snapshot-format.md +++ b/web/snapshot-format.md @@ -11,26 +11,28 @@ The snapshot format is a compact binary representation of terminal buffer state, ``` ┌──────────────┬─────────────────────────────────┐ │ Header │ Cell Stream │ -│ (16 bytes) │ (variable, 4+ bytes/cell) │ +│ (32 bytes) │ (variable, 4+ bytes/cell) │ └──────────────┴─────────────────────────────────┘ ``` -## Header Format (16 bytes) +## Header Format (32 bytes) - Version 2 ``` Offset Size Field Description ------ ---- ---------- ----------- 0x00 2 Magic 0x5654 ("VT" in ASCII) -0x02 1 Version Format version (currently 0x01) +0x02 1 Version Format version (0x02 for 32-bit support) 0x03 1 Flags Reserved for future use -0x04 2 Cols Terminal width (little-endian) -0x06 2 Rows Number of rows in this snapshot (little-endian) -0x08 2 ViewportY Starting line number in buffer (little-endian) -0x0A 2 CursorX Cursor column position (little-endian) -0x0C 2 CursorY Cursor row position relative to viewport (little-endian) -0x0E 2 Reserved Reserved for future use +0x04 4 Cols Terminal width (32-bit unsigned, little-endian) +0x08 4 Rows Number of rows in this snapshot (32-bit unsigned, little-endian) +0x0C 4 ViewportY Starting line number in buffer (32-bit signed, little-endian) +0x10 4 CursorX Cursor column position (32-bit signed, little-endian) +0x14 4 CursorY Cursor row position relative to viewport (32-bit signed, little-endian) +0x18 4 Reserved Reserved for future use ``` +Note: CursorY is relative to the viewport and can be negative if the cursor is above the visible area. + ## Cell Format Each cell uses a variable-length encoding: @@ -131,16 +133,17 @@ Content-Length: {size} For a 80x24 terminal showing "Hello" on black background: ``` -Header (16 bytes): -56 54 01 00 50 00 18 00 00 00 05 00 00 00 00 00 -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─ Reserved -│ │ │ │ │ │ │ │ │ │ │ └───┴─┴─ Cursor (5,0) -│ │ │ │ │ │ │ │ └─┴─ ViewportY (0) -│ │ │ │ │ │ └─┴─ Rows (24) -│ │ │ │ └─┴─ Cols (80) +Header (32 bytes): +56 54 02 00 50 00 00 00 18 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ Reserved +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ CursorY (0) +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ CursorX (5) +│ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ ViewportY (0) +│ │ │ │ │ │ │ │ └─┴─┴─┴─ Rows (24) +│ │ │ │ └─┴─┴─┴─ Cols (80) │ │ │ └─ Flags (0) -│ │ └─ Version (1) +│ │ └─ Version (2) └─┴─ Magic "VT" Cells: diff --git a/web/src/client/utils/terminal-renderer.ts b/web/src/client/utils/terminal-renderer.ts index 9b812df4..6faa73fb 100644 --- a/web/src/client/utils/terminal-renderer.ts +++ b/web/src/client/utils/terminal-renderer.ts @@ -286,22 +286,22 @@ export class TerminalRenderer { } const version = view.getUint8(offset++); - if (version !== 0x01) { - throw new Error('Unsupported buffer version'); + if (version !== 0x02) { + throw new Error(`Unsupported buffer version: ${version}`); } const _flags = view.getUint8(offset++); - const cols = view.getUint16(offset, true); - offset += 2; - const rows = view.getUint16(offset, true); - offset += 2; - const viewportY = view.getUint16(offset, true); - offset += 2; - const cursorX = view.getUint16(offset, true); - offset += 2; - const cursorY = view.getUint16(offset, true); - offset += 2; - offset += 2; // Skip reserved + const cols = view.getUint32(offset, true); + offset += 4; + const rows = view.getUint32(offset, true); + offset += 4; + const viewportY = view.getInt32(offset, true); // Signed + offset += 4; + const cursorX = view.getInt32(offset, true); // Signed + offset += 4; + const cursorY = view.getInt32(offset, true); // Signed + offset += 4; + offset += 4; // Skip reserved // Decode cells const cells: BufferCell[][] = []; diff --git a/web/src/terminal-manager.ts b/web/src/terminal-manager.ts index b28c7411..c475a646 100644 --- a/web/src/terminal-manager.ts +++ b/web/src/terminal-manager.ts @@ -301,29 +301,29 @@ export class TerminalManager { const { cols, rows, viewportY, cursorX, cursorY, cells } = snapshot; // Calculate buffer size (rough estimate) - const estimatedSize = 16 + rows * cols * 4; + const estimatedSize = 32 + rows * cols * 4; // Increased header size const buffer = Buffer.allocUnsafe(estimatedSize); let offset = 0; - // Write header (16 bytes) + // Write header (32 bytes) buffer.writeUInt16LE(0x5654, offset); offset += 2; // Magic "VT" - buffer.writeUInt8(0x01, offset); + buffer.writeUInt8(0x02, offset); // Version 2 with 32-bit values offset += 1; // Version buffer.writeUInt8(0x00, offset); offset += 1; // Flags - buffer.writeUInt16LE(cols, offset); - offset += 2; // Cols - buffer.writeUInt16LE(rows, offset); - offset += 2; // Rows - buffer.writeUInt16LE(viewportY, offset); - offset += 2; // ViewportY - buffer.writeUInt16LE(cursorX, offset); - offset += 2; // CursorX - buffer.writeUInt16LE(cursorY, offset); - offset += 2; // CursorY - buffer.writeUInt16LE(0, offset); - offset += 2; // Reserved + buffer.writeUInt32LE(cols, offset); + offset += 4; // Cols (32-bit) + buffer.writeUInt32LE(rows, offset); + offset += 4; // Rows (32-bit) + buffer.writeInt32LE(viewportY, offset); // Signed for large buffers + offset += 4; // ViewportY (32-bit signed) + buffer.writeInt32LE(cursorX, offset); // Signed for consistency + offset += 4; // CursorX (32-bit signed) + buffer.writeInt32LE(cursorY, offset); // Signed for relative positions + offset += 4; // CursorY (32-bit signed) + buffer.writeUInt32LE(0, offset); + offset += 4; // Reserved // Write cells with run-length encoding let lastCell: BufferCell | null = null;