Fix binary buffer encoding to support large terminals

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 <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-20 02:31:15 +02:00
parent b041743287
commit ef7a679c2b
3 changed files with 49 additions and 46 deletions

View file

@ -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:

View file

@ -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[][] = [];

View file

@ -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;