diff --git a/src/terminal/snapshot/history.zig b/src/terminal/snapshot/history.zig new file mode 100644 index 000000000..d52f980fa --- /dev/null +++ b/src/terminal/snapshot/history.zig @@ -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(); + } +} diff --git a/src/terminal/snapshot/main.zig b/src/terminal/snapshot/main.zig index 8d0658cbd..d7f3f7e34 100644 --- a/src/terminal/snapshot/main.zig +++ b/src/terminal/snapshot/main.zig @@ -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"); diff --git a/src/terminal/snapshot/record.zig b/src/terminal/snapshot/record.zig index 3694173f1..6a5a20e70 100644 --- a/src/terminal/snapshot/record.zig +++ b/src/terminal/snapshot/record.zig @@ -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, diff --git a/src/terminal/snapshot/screen.zig b/src/terminal/snapshot/screen.zig index bb61af5b8..5b9d23bc5 100644 --- a/src/terminal/snapshot/screen.zig +++ b/src/terminal/snapshot/screen.zig @@ -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: