terminal/snapshot: history record

This commit is contained in:
Mitchell Hashimoto
2026-07-30 08:20:26 -07:00
parent d34fd0593e
commit 7d91b87766
4 changed files with 776 additions and 13 deletions

View File

@@ -0,0 +1,730 @@
//! HISTORY record payload encoding.
//!
//! One HISTORY record describes the complete history for a previously encoded
//! screen. It is followed immediately by the number of complete PAGE records
//! declared by `page_count`. Those pages contain the history older than the
//! first page in the corresponding SCREEN sequence and are ordered from newest
//! to oldest.
//!
//! Every encoded SCREEN has one corresponding HISTORY record. A screen with no
//! history uses zero for all three count fields and has no following PAGE
//! records.
//!
//! The first SCREEN page may begin above the active area. Those incidental
//! history rows are counted by `screen_overlap_rows`; they are not repeated
//! after HISTORY. `total_rows` is the authoritative number of logical rows
//! before the active area, including both the SCREEN overlap and the rows in
//! the following PAGE records.
//!
//! All integers are unsigned and little-endian.
//!
//! ## Binary Format
//!
//! A HISTORY record is followed immediately by its declared PAGE records:
//!
//! ```text
//! +----------------------+
//! | HISTORY record |
//! +----------------------+
//! | PAGE record 0 | newest
//! +----------------------+
//! | ... |
//! +----------------------+
//! | PAGE record (n - 1) | oldest
//! +----------------------+
//!
//! n = page_count
//! ```
//!
//! Newest-to-oldest order lets a reader start showing most-recent
//! history as soon as possible which is more useful to a user.
//!
//! The PAGE sequence may be empty when all history is already included in the
//! first SCREEN page or when the screen has no history.
//!
//! ### Header
//!
//! The HISTORY payload consists only of this fixed header:
//!
//! ```text
//! 0 +--------------------------------+
//! | Screen key (u16) |
//! 2 +--------------------------------+
//! | Following page count (u32) |
//! 6 +--------------------------------+
//! | Total logical history rows |
//! | u64 |
//! 14 +--------------------------------+
//! | SCREEN overlap rows (u16) |
//! 16 +--------------------------------+
//! ```
//!
//! The sum of `screen_overlap_rows` and the logical row counts of all
//! following PAGE records must equal `total_rows`. A decoder uses `page_count`,
//! rather than another record tag, to find the end of the page sequence.
const std = @import("std");
const Allocator = std.mem.Allocator;
const io = @import("io.zig");
const page = @import("page.zig");
const record = @import("record.zig");
const screen = @import("screen.zig");
const TerminalPage = @import("../page.zig").Page;
const TerminalPageList = @import("../PageList.zig");
const TerminalScreen = @import("../Screen.zig");
const TerminalScreenKey = @import("../ScreenSet.zig").Key;
/// The complete fixed payload of one HISTORY record.
pub const Header = struct {
/// Number of encoded bytes in the fixed payload, calculated by its encoder.
pub const len = computeLen();
comptime {
// This size is part of the wire format. If it changes, the snapshot
// version must also change.
std.debug.assert(len == 16);
}
/// Identifies the previously encoded screen that owns this history.
key: screen.Key,
/// Number of complete PAGE records immediately following this record.
page_count: u32,
/// Authoritative number of logical rows before the active area.
total_rows: u64,
/// History rows already included at the start of the first SCREEN page.
screen_overlap_rows: u16,
/// Encode the fixed HISTORY payload.
pub fn encode(
self: Header,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
try io.writeInt(writer, u16, @intFromEnum(self.key));
try io.writeInt(writer, u32, self.page_count);
try io.writeInt(writer, u64, self.total_rows);
try io.writeInt(writer, u16, self.screen_overlap_rows);
}
pub const DecodeError = std.Io.Reader.Error || error{InvalidKey};
/// Decode and validate the fixed HISTORY payload.
pub fn decode(reader: *std.Io.Reader) Header.DecodeError!Header {
const key = std.enums.fromInt(
screen.Key,
try io.readInt(reader, u16),
) orelse return error.InvalidKey;
return .{
.key = key,
.page_count = try io.readInt(reader, u32),
.total_rows = try io.readInt(reader, u64),
.screen_overlap_rows = try io.readInt(reader, u16),
};
}
fn computeLen() usize {
comptime {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const value: Header = .{
.key = .primary,
.page_count = 0,
.total_rows = 0,
.screen_overlap_rows = 0,
};
value.encode(&writer) catch unreachable;
return writer.end;
}
}
};
/// Errors possible while encoding one HISTORY and its complete PAGE sequence.
pub const EncodeError = Allocator.Error ||
page.EncodeError ||
record.Writer.FinishError ||
error{
/// The complete historical prefix has more pages than the header fits.
PageCountOverflow,
/// The complete historical prefix has more rows than the header fits.
RowCountOverflow,
};
/// Encode one screen's HISTORY and its complete historical pages.
///
/// Pages are encoded newest-to-oldest. Compressed source pages are inspected
/// without changing their storage state. On failure, the entire sequence is
/// removed while earlier destination bytes remain.
pub fn encode(
terminal_screen: *const TerminalScreen,
key: TerminalScreenKey,
destination: *std.Io.Writer.Allocating,
) EncodeError!void {
const sequence_start = destination.written().len;
errdefer destination.shrinkRetainingCapacity(sequence_start);
// SCREEN begins at the page containing the active area's first row. Its
// leading rows are already resident; every previous complete page belongs
// to this HISTORY sequence.
const active_top = terminal_screen.pages.getTopLeft(.active);
const page_count: usize, const total_rows: u64 = count: {
var page_count: usize = 0;
var total_rows: u64 = active_top.y;
var node = active_top.node.prev;
while (node) |current| : (node = current.prev) {
page_count += 1;
total_rows = std.math.add(
u64,
total_rows,
current.rows(),
) catch return error.RowCountOverflow;
}
break :count .{ page_count, total_rows };
};
const header: Header = .{
.key = switch (key) {
.primary => .primary,
.alternate => .alternate,
},
.page_count = std.math.cast(
u32,
page_count,
) orelse return error.PageCountOverflow,
.total_rows = total_rows,
.screen_overlap_rows = active_top.y,
};
// HISTORY declares exactly how many PAGE records follow and how their rows
// combine with the overlap already carried by SCREEN.
{
var record_writer = try record.Writer.init(destination, .history);
errdefer record_writer.cancel();
try header.encode(record_writer.payloadWriter());
try record_writer.finish();
}
// Walk backward so each page can be prepended by the decoder immediately.
// PreservedPage borrows resident pages and clones compressed pages without
// changing the source node's representation.
var node = active_top.node.prev;
while (node) |current| : (node = current.prev) {
var preserved = try current.pagePreservingState(terminal_screen.alloc);
defer preserved.deinit();
try page.encode(preserved.page(), destination);
}
}
/// Errors possible while restoring one HISTORY and its PAGE sequence.
pub const DecodeError = Allocator.Error ||
Header.DecodeError ||
page.DecodeError ||
record.Reader.InitError ||
record.Reader.FinishError ||
TerminalPageList.PageAllocation.FinalizeError ||
error{
/// The next record is valid but is not a HISTORY.
UnexpectedRecordTag,
/// The HISTORY key does not match the caller-selected screen.
UnexpectedScreenKey,
/// The Screen already contains complete pages before its active page.
ExistingHistory,
/// The claimed overlap does not match the restored SCREEN sequence.
InvalidScreenOverlap,
/// The declared total does not match the restored historical rows.
InvalidHistoryRows,
};
/// Restore one HISTORY and its declared PAGE records into a native Screen.
///
/// Each PAGE is decoded directly into a detached PageList-pooled allocation and
/// prepended only after its record and native integrity are validated. If a
/// later PAGE fails, earlier successful pages remain as a contiguous recent
/// history prefix.
pub fn decode(
source: *std.Io.Reader,
alloc: Allocator,
expected_key: TerminalScreenKey,
terminal_screen: *TerminalScreen,
) DecodeError!void {
// Decode and finish the self-contained HISTORY manifest before consuming
// any of its following PAGE records.
const header: Header = header: {
var record_reader: record.Reader = undefined;
try record_reader.init(source);
if (record_reader.header.tag != .history) return error.UnexpectedRecordTag;
const header = try Header.decode(record_reader.payloadReader());
try record_reader.finish();
break :header header;
};
const decoded_key: TerminalScreenKey = switch (header.key) {
.primary => .primary,
.alternate => .alternate,
};
if (decoded_key != expected_key) return error.UnexpectedScreenKey;
// A freshly restored SCREEN may carry overlap inside its first page, but
// cannot already contain a complete historical page before that boundary.
const active_top = terminal_screen.pages.getTopLeft(.active);
if (terminal_screen.pages.getTopLeft(.screen).node != active_top.node) {
return error.ExistingHistory;
}
if (header.screen_overlap_rows != active_top.y) {
return error.InvalidScreenOverlap;
}
if (header.total_rows < header.screen_overlap_rows) {
return error.InvalidHistoryRows;
}
var restored_rows: u64 = header.screen_overlap_rows;
for (0..header.page_count) |_| {
// PAGE exposes its exact capacity before decoding the payload, allowing
// the destination PageList to allocate the final backing memory once.
var decoder: page.Decoder = undefined;
try decoder.init(source);
var allocation = try terminal_screen.pages.allocatePage(
decoder.capacity(),
);
defer allocation.deinit();
try decoder.decode(allocation.page(), alloc);
// Validate the authoritative row total before publishing this page.
const page_rows = allocation.page().size.rows;
restored_rows = std.math.add(
u64,
restored_rows,
page_rows,
) catch return error.InvalidHistoryRows;
if (restored_rows > header.total_rows) {
return error.InvalidHistoryRows;
}
const contains_prompt = hasSemanticPrompt(allocation.page());
try allocation.finalize(.prepend);
if (contains_prompt) terminal_screen.semantic_prompt.seen = true;
}
if (restored_rows != header.total_rows) {
return error.InvalidHistoryRows;
}
terminal_screen.pages.assertIntegrity();
terminal_screen.assertIntegrity();
}
fn hasSemanticPrompt(terminal_page: *const TerminalPage) bool {
const rows = terminal_page.rows.ptr(
terminal_page.memory,
)[0..terminal_page.size.rows];
for (rows) |row| {
if (row.semantic_prompt != .none) return true;
const cells = row.cells.ptr(
terminal_page.memory,
)[0..terminal_page.size.cols];
for (cells) |cell| {
if (cell.semantic_content == .prompt) return true;
}
}
return false;
}
test "HISTORY header golden encoding and decoding" {
const expected: Header = .{
.key = .alternate,
.page_count = 0x01020304,
.total_rows = 0x0102030405060708,
.screen_overlap_rows = 0x090a,
};
const fixture =
"\x01\x00\x04\x03\x02\x01" ++
"\x08\x07\x06\x05\x04\x03\x02\x01\x0a\x09";
try std.testing.expectEqual(Header.len, fixture.len);
var encoded: [Header.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try expected.encode(&writer);
try std.testing.expectEqualStrings(fixture, writer.buffered());
var reader: std.Io.Reader = .fixed(fixture);
try std.testing.expectEqualDeep(expected, try Header.decode(&reader));
for (0..Header.len) |fixture_len| {
var truncated: std.Io.Reader = .fixed(fixture[0..fixture_len]);
try std.testing.expectError(
error.EndOfStream,
Header.decode(&truncated),
);
}
}
test "HISTORY encodes newest first and restores complete history" {
// Choose an active height which spans two native pages, then grow until
// exactly two additional complete pages precede the active boundary.
var probe = try TerminalScreen.init(
std.testing.io,
std.testing.allocator,
.{ .cols = 80, .rows = 1, .max_scrollback_bytes = 0 },
);
const page_rows = probe.pages.getTopLeft(.screen).node.capacity().rows;
probe.deinit();
const screen_rows = page_rows + 1;
var source_screen = try TerminalScreen.init(
std.testing.io,
std.testing.allocator,
.{
.cols = 80,
.rows = screen_rows,
.max_scrollback_bytes = null,
},
);
defer source_screen.deinit();
source_screen.cursorAbsolute(0, screen_rows - 1);
while (source_screen.pages.totalPages() < 4) {
try source_screen.testWriteString("\n");
}
const active_top = source_screen.pages.getTopLeft(.active);
const newest_history = active_top.node.prev.?;
const oldest_history = newest_history.prev.?;
try std.testing.expectEqual(
source_screen.pages.getTopLeft(.screen).node,
oldest_history,
);
try std.testing.expectEqual(null, oldest_history.prev);
// Mark the two complete historical pages so the wire sequence and final
// native order are visible. The oldest prompt also verifies derived state
// is updated only when that page is restored.
oldest_history.page().getRowAndCell(0, 0).cell.* = .init('A');
oldest_history.page().getRowAndCell(
0,
0,
).cell.semantic_content = .prompt;
newest_history.page().getRowAndCell(0, 0).cell.* = .init('B');
// Encode SCREEN before history compression, then compress eligible history
// and require HISTORY encoding to preserve each source storage state.
var destination: std.Io.Writer.Allocating = .init(
std.testing.allocator,
);
defer destination.deinit();
try screen.encode(&source_screen, .primary, &destination);
const history_offset = destination.written().len;
_ = source_screen.pages.compress(.full);
const oldest_storage = oldest_history.storage();
const newest_storage = newest_history.storage();
try encode(&source_screen, .primary, &destination);
try std.testing.expectEqual(oldest_storage, oldest_history.storage());
try std.testing.expectEqual(newest_storage, newest_history.storage());
// Inspect the manifest and PAGE records independently. Newest history is
// sent first even though native PageList order is oldest-to-newest.
var history_source: std.Io.Reader = .fixed(
destination.written()[history_offset..],
);
var history_record: record.Reader = undefined;
try history_record.init(&history_source);
try std.testing.expectEqual(record.Tag.history, history_record.header.tag);
const header = try Header.decode(history_record.payloadReader());
try history_record.finish();
try std.testing.expectEqual(screen.Key.primary, header.key);
try std.testing.expectEqual(@as(u32, 2), header.page_count);
try std.testing.expectEqual(
@as(u16, active_top.y),
header.screen_overlap_rows,
);
try std.testing.expectEqual(
@as(u64, active_top.y) +
oldest_history.rows() +
newest_history.rows(),
header.total_rows,
);
var decoded_newest = try page.decode(
&history_source,
std.testing.allocator,
);
defer decoded_newest.deinit();
try std.testing.expectEqual(
@as(u21, 'B'),
decoded_newest.getRowAndCell(0, 0).cell.codepoint(),
);
var decoded_oldest = try page.decode(
&history_source,
std.testing.allocator,
);
defer decoded_oldest.deinit();
try std.testing.expectEqual(
@as(u21, 'A'),
decoded_oldest.getRowAndCell(0, 0).cell.codepoint(),
);
try std.testing.expectError(error.EndOfStream, history_source.takeByte());
// Restore SCREEN first, then prepend HISTORY directly into that PageList.
var restore_source: std.Io.Reader = .fixed(destination.written());
var restored = try screen.decode(
&restore_source,
std.testing.io,
std.testing.allocator,
.primary,
.{
.cols = 80,
.rows = screen_rows,
.max_scrollback_bytes = null,
},
);
defer restored.deinit();
try std.testing.expect(!restored.semantic_prompt.seen);
try decode(
&restore_source,
std.testing.allocator,
.primary,
&restored,
);
try std.testing.expectEqual(
source_screen.pages.totalPages(),
restored.pages.totalPages(),
);
const restored_oldest = restored.pages.getTopLeft(.screen).node;
try std.testing.expectEqual(
@as(u21, 'A'),
restored_oldest.page().getRowAndCell(0, 0).cell.codepoint(),
);
try std.testing.expectEqual(
@as(u21, 'B'),
restored_oldest.next.?
.page().getRowAndCell(0, 0).cell.codepoint(),
);
try std.testing.expect(restored.semantic_prompt.seen);
try std.testing.expectError(error.EndOfStream, restore_source.takeByte());
// Reuse a writable copy of the sequence for failure-path fixtures.
const encoded = try std.testing.allocator.dupe(
u8,
destination.written(),
);
defer std.testing.allocator.free(encoded);
const first_page_offset = history_offset + record.Header.len + Header.len;
const first_payload_len = std.mem.readInt(
u32,
encoded[first_page_offset + 2 ..][0..4],
.little,
);
const second_page_offset =
first_page_offset + record.Header.len + first_payload_len;
// Truncate the first PAGE only after its header has exposed a capacity.
// Its detached allocation is discarded and the live SCREEN list remains
// completely unchanged.
var truncated_source: std.Io.Reader = .fixed(
encoded[0 .. second_page_offset - 1],
);
var truncated = try screen.decode(
&truncated_source,
std.testing.io,
std.testing.allocator,
.primary,
.{
.cols = 80,
.rows = screen_rows,
.max_scrollback_bytes = null,
},
);
defer truncated.deinit();
const truncated_screen_first = truncated.pages.getTopLeft(.screen).node;
const truncated_screen_page_count = truncated.pages.totalPages();
try std.testing.expectError(
error.EndOfStream,
decode(
&truncated_source,
std.testing.allocator,
.primary,
&truncated,
),
);
try std.testing.expectEqual(
truncated_screen_page_count,
truncated.pages.totalPages(),
);
try std.testing.expectEqual(
truncated_screen_first,
truncated.pages.getTopLeft(.screen).node,
);
truncated.pages.assertIntegrity();
truncated.assertIntegrity();
// Corrupt only the older PAGE tag. A failure in a later PAGE keeps only
// the successfully prepended newer pages, contiguous with SCREEN.
std.mem.writeInt(
u16,
encoded[second_page_offset..][0..2],
@intFromEnum(record.Tag.screen),
.little,
);
var partial_source: std.Io.Reader = .fixed(encoded);
var partial = try screen.decode(
&partial_source,
std.testing.io,
std.testing.allocator,
.primary,
.{
.cols = 80,
.rows = screen_rows,
.max_scrollback_bytes = null,
},
);
defer partial.deinit();
const screen_page_count = partial.pages.totalPages();
try std.testing.expectError(
error.UnexpectedRecordTag,
decode(
&partial_source,
std.testing.allocator,
.primary,
&partial,
),
);
try std.testing.expectEqual(
screen_page_count + 1,
partial.pages.totalPages(),
);
try std.testing.expectEqual(
@as(u21, 'B'),
partial.pages.getTopLeft(.screen).node
.page().getRowAndCell(0, 0).cell.codepoint(),
);
try std.testing.expect(!partial.semantic_prompt.seen);
partial.pages.assertIntegrity();
partial.assertIntegrity();
}
test "HISTORY encodes and restores an empty sequence" {
var terminal_screen = try TerminalScreen.init(
std.testing.io,
std.testing.allocator,
.{
.cols = 2,
.rows = 2,
.max_scrollback_bytes = null,
},
);
defer terminal_screen.deinit();
var destination: std.Io.Writer.Allocating = .init(
std.testing.allocator,
);
defer destination.deinit();
try encode(&terminal_screen, .primary, &destination);
var inspect_source: std.Io.Reader = .fixed(destination.written());
var history_record: record.Reader = undefined;
try history_record.init(&inspect_source);
const header = try Header.decode(history_record.payloadReader());
try history_record.finish();
try std.testing.expectEqual(@as(u32, 0), header.page_count);
try std.testing.expectEqual(@as(u64, 0), header.total_rows);
try std.testing.expectEqual(@as(u16, 0), header.screen_overlap_rows);
try std.testing.expectError(error.EndOfStream, inspect_source.takeByte());
var decode_source: std.Io.Reader = .fixed(destination.written());
const initial_first = terminal_screen.pages.getTopLeft(.screen).node;
try decode(
&decode_source,
std.testing.allocator,
.primary,
&terminal_screen,
);
try std.testing.expectEqual(
initial_first,
terminal_screen.pages.getTopLeft(.screen).node,
);
try std.testing.expectError(error.EndOfStream, decode_source.takeByte());
}
test "HISTORY rejects manifest mismatches" {
var terminal_screen = try TerminalScreen.init(
std.testing.io,
std.testing.allocator,
.{
.cols = 2,
.rows = 2,
.max_scrollback_bytes = null,
},
);
defer terminal_screen.deinit();
// Encode a manifest without PAGE records and vary only the fixed fields so
// each structural validation boundary is exercised in isolation.
const cases = .{
.{
Header{
.key = .alternate,
.page_count = 0,
.total_rows = 0,
.screen_overlap_rows = 0,
},
error.UnexpectedScreenKey,
},
.{
Header{
.key = .primary,
.page_count = 0,
.total_rows = 1,
.screen_overlap_rows = 1,
},
error.InvalidScreenOverlap,
},
.{
Header{
.key = .primary,
.page_count = 0,
.total_rows = 1,
.screen_overlap_rows = 0,
},
error.InvalidHistoryRows,
},
.{
Header{
.key = .primary,
.page_count = 1,
.total_rows = 1,
.screen_overlap_rows = 0,
},
error.EndOfStream,
},
};
inline for (cases) |case| {
var destination: std.Io.Writer.Allocating = .init(
std.testing.allocator,
);
defer destination.deinit();
var record_writer = try record.Writer.init(&destination, .history);
try case[0].encode(record_writer.payloadWriter());
try record_writer.finish();
var source: std.Io.Reader = .fixed(destination.written());
try std.testing.expectError(
case[1],
decode(
&source,
std.testing.allocator,
.primary,
&terminal_screen,
),
);
terminal_screen.pages.assertIntegrity();
terminal_screen.assertIntegrity();
}
}

View File

@@ -7,11 +7,16 @@
//!
//! We call this a "snapshot." The snapshot is purposely laid out in a way
//! that prioritizes making a terminal functional as quickly as possible.
//! To do that, it sends down the terminal state, viewport, etc. followed
//! by a "READY" event. At the READY state, the terminal is functional and
//! could in theory begin processing pty bytes. After the READY state the
//! binary format continues transmitting history and extra assets such as
//! images and so on.
//! To do that, it sends the active terminal state followed by a READY record,
//! then complete history.
//!
//! READY denotes that enough of the terminal state is down that it can
//! be fully rendered at that point. This is also the point where live
//! terminals can also start accepting pty bytes, typically. But the current
//! snapshot format lacks some of the information necessary to synchronize
//! pty byte state with an authoritative server.
//!
//! After READY, we send history pages (scrollback).
//!
//! ## Snapshot Format
//!
@@ -39,9 +44,35 @@
//! +------------------+
//! ```
//!
//! Records have a strict order: TERMINAL, the primary SCREEN and its
//! PAGE records, an optional alternate SCREEN and its PAGE records,
//! CONTINUATION, READY, and FINISH.
//! Records have a strict order:
//!
//! ```text
//! +----------------------------------------+
//! | TERMINAL |
//! +----------------------------------------+
//! | SCREEN (primary) |
//! | PAGE * screen.page_count |
//! +----------------------------------------+
//! | SCREEN (alternate, when present) |
//! | PAGE * screen.page_count |
//! +----------------------------------------+
//! | READY |
//! +----------------------------------------+
//! | HISTORY (primary) |
//! | PAGE * history.page_count |
//! +----------------------------------------+
//! | HISTORY (alternate, when present) |
//! | PAGE * history.page_count |
//! +----------------------------------------+
//! | FINISH |
//! +----------------------------------------+
//! ```
//!
//! The SCREEN sequences contain the complete pages needed to restore each
//! active area. A HISTORY sequence contains the older complete pages for its
//! screen in newest-to-oldest order so they can be prepended as they arrive.
//! Every SCREEN has one corresponding HISTORY, even when its history page count
//! is zero. FINISH is followed by end-of-file.
//!
//! ## Encoding
//!
@@ -66,6 +97,7 @@
pub const envelope = @import("envelope.zig");
pub const grid = @import("grid.zig");
pub const history = @import("history.zig");
pub const hyperlink = @import("hyperlink.zig");
pub const page = @import("page.zig");
pub const record = @import("record.zig");

View File

@@ -37,8 +37,8 @@ pub const Tag = enum(u16) {
/// One complete logical terminal page.
page = 3,
/// Unfinished UTF-8 and terminal parser input.
continuation = 4,
/// One screen's complete history manifest and page sequence.
history = 4,
/// Digest marking the validated terminal-state prefix.
ready = 5,

View File

@@ -37,9 +37,10 @@
//! n = page_count
//! ```
//!
//! A later history-capable snapshot version provides the authoritative history
//! manifest. It counts any incidental prefix above as already resident and
//! describes older rows that may be loaded after the terminal becomes ready.
//! The HISTORY record for this screen provides the authoritative history
//! manifest. It counts any incidental prefix above as the SCREEN overlap, then
//! sends the older complete pages after the terminal becomes ready. The prefix
//! is not sent again.
//!
//! The SCREEN payload begins with a fixed header. When the header says there is
//! no saved cursor, the payload is: