mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 20:59:02 +00:00
terminal/snapshot: move history size hints to screen record
Publish each screen's logical history extent before READY so clients can size scrollbars while older pages are still arriving. Keep the value advisory and continue deriving native PageList totals from decoded pages. Reduce HISTORY to its structural screen key and page count, and update the format documentation, Kaitai schema, verifier, and versioned fixtures.
This commit is contained in:
@@ -7,15 +7,13 @@
|
||||
//! 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.
|
||||
//! older pages uses a zero page count 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 canonical number of logical rows before
|
||||
//! the active area, including both the SCREEN overlap and the rows in the
|
||||
//! following PAGE records. Decoders derive native row totals from the restored
|
||||
//! pages and may ignore inconsistent row metadata.
|
||||
//! history rows are already present at READY and are not repeated after
|
||||
//! HISTORY. The corresponding SCREEN header declares the complete logical
|
||||
//! history extent, including both that resident overlap and the following PAGE
|
||||
//! records.
|
||||
//!
|
||||
//! All integers are unsigned and little-endian.
|
||||
//!
|
||||
@@ -53,17 +51,10 @@
|
||||
//! 2 +--------------------------------+
|
||||
//! | Following page count (u32) |
|
||||
//! 6 +--------------------------------+
|
||||
//! | Total logical history rows |
|
||||
//! | u64 |
|
||||
//! 14 +--------------------------------+
|
||||
//! | SCREEN overlap rows (u16) |
|
||||
//! 16 +--------------------------------+
|
||||
//! ```
|
||||
//!
|
||||
//! A canonical encoder sets `total_rows` to the sum of `screen_overlap_rows`
|
||||
//! and the logical row counts of all following PAGE records. A decoder uses
|
||||
//! `page_count`, rather than another record tag, to find the end of the page
|
||||
//! sequence.
|
||||
//! 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;
|
||||
@@ -85,7 +76,7 @@ pub const Header = struct {
|
||||
comptime {
|
||||
// This size is part of the wire format. If it changes, the snapshot
|
||||
// version must also change.
|
||||
std.debug.assert(len == 16);
|
||||
std.debug.assert(len == 6);
|
||||
}
|
||||
|
||||
/// Identifies the previously encoded screen that owns this history.
|
||||
@@ -94,12 +85,6 @@ pub const Header = struct {
|
||||
/// Number of complete PAGE records immediately following this record.
|
||||
page_count: u32,
|
||||
|
||||
/// Canonical 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,
|
||||
@@ -107,8 +92,6 @@ pub const Header = struct {
|
||||
) std.Io.Writer.Error!void {
|
||||
try io.writeInt(writer, u16, @intCast(@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};
|
||||
@@ -126,8 +109,6 @@ pub const Header = struct {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,8 +119,6 @@ pub const Header = struct {
|
||||
const value: Header = .{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 0,
|
||||
.screen_overlap_rows = 0,
|
||||
};
|
||||
value.encode(&writer) catch unreachable;
|
||||
return writer.end;
|
||||
@@ -154,9 +133,6 @@ pub const EncodeError = Allocator.Error ||
|
||||
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.
|
||||
@@ -172,21 +148,14 @@ pub fn encode(
|
||||
// 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: {
|
||||
const first = terminal_screen.pages.getTopLeft(.active).node.prev;
|
||||
const page_count: usize = count: {
|
||||
var page_count: usize = 0;
|
||||
var total_rows: u64 = active_top.y;
|
||||
var node = active_top.node.prev;
|
||||
var node = first;
|
||||
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 };
|
||||
break :count page_count;
|
||||
};
|
||||
|
||||
const header: Header = .{
|
||||
@@ -195,12 +164,9 @@ pub fn encode(
|
||||
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.
|
||||
// HISTORY declares exactly how many PAGE records follow.
|
||||
{
|
||||
const payload = destination.begin(.history);
|
||||
errdefer destination.cancel();
|
||||
@@ -211,7 +177,7 @@ pub fn encode(
|
||||
// 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;
|
||||
var node = first;
|
||||
while (node) |current| : (node = current.prev) {
|
||||
var preserved = try current.pagePreservingState(terminal_screen.alloc);
|
||||
defer preserved.deinit();
|
||||
@@ -277,9 +243,7 @@ pub const Decoder = struct {
|
||||
return error.ExistingHistory;
|
||||
}
|
||||
|
||||
// Row metadata is redundant with the restored SCREEN and PAGE
|
||||
// topology. Consume the declared PAGE records, but derive all native
|
||||
// row totals from their actual dimensions.
|
||||
// Native row totals remain derived from the actual PAGE dimensions.
|
||||
for (0..self.header.page_count) |_| {
|
||||
// PAGE exposes its exact capacity before decoding the payload,
|
||||
// allowing the destination PageList to allocate the final backing
|
||||
@@ -341,8 +305,6 @@ test "HISTORY header golden encoding and decoding" {
|
||||
const expected: Header = .{
|
||||
.key = .alternate,
|
||||
.page_count = 0x01020304,
|
||||
.total_rows = 0x0102030405060708,
|
||||
.screen_overlap_rows = 0x090a,
|
||||
};
|
||||
var encoded: [Header.len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
@@ -456,16 +418,6 @@ test "HISTORY encodes newest first and restores complete history" {
|
||||
try history_record.finish();
|
||||
try std.testing.expectEqual(TerminalScreenKey.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,
|
||||
@@ -504,6 +456,10 @@ test "HISTORY encodes newest first and restores complete history" {
|
||||
TerminalScreenKey.primary,
|
||||
decoded_screen.key,
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
@as(u64, @intCast(source_screen.pages.total_rows - screen_rows)),
|
||||
decoded_screen.history_rows,
|
||||
);
|
||||
const restored = &decoded_screen.screen;
|
||||
try std.testing.expect(!restored.semantic_prompt.seen);
|
||||
try decode(
|
||||
@@ -660,8 +616,6 @@ test "HISTORY encodes and restores an empty sequence" {
|
||||
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());
|
||||
@@ -679,7 +633,7 @@ test "HISTORY encodes and restores an empty sequence" {
|
||||
try std.testing.expectError(error.EndOfStream, decode_source.takeByte());
|
||||
}
|
||||
|
||||
test "HISTORY accepts row metadata mismatches and rejects structural ones" {
|
||||
test "HISTORY rejects invalid routing and incomplete sequences" {
|
||||
var terminal_screen = try TerminalScreen.init(
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
@@ -691,49 +645,6 @@ test "HISTORY accepts row metadata mismatches and rejects structural ones" {
|
||||
);
|
||||
defer terminal_screen.deinit();
|
||||
|
||||
// Row counts are redundant metadata. Even internally inconsistent values
|
||||
// do not prevent restoring the topology declared by page_count.
|
||||
const accepted = [_]Header{
|
||||
.{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 0,
|
||||
.screen_overlap_rows = 1,
|
||||
},
|
||||
.{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = std.math.maxInt(u64),
|
||||
.screen_overlap_rows = std.math.maxInt(u16),
|
||||
},
|
||||
};
|
||||
for (accepted) |header| {
|
||||
var destination: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
);
|
||||
defer destination.deinit();
|
||||
var stream: record.Writer = .init(
|
||||
std.testing.allocator,
|
||||
&destination.writer,
|
||||
);
|
||||
defer stream.deinit();
|
||||
const payload = stream.begin(.history);
|
||||
errdefer stream.cancel();
|
||||
try header.encode(payload);
|
||||
try stream.finish();
|
||||
|
||||
var source: std.Io.Reader = .fixed(destination.written());
|
||||
try decode(
|
||||
&source,
|
||||
std.testing.allocator,
|
||||
.primary,
|
||||
&terminal_screen,
|
||||
);
|
||||
try std.testing.expectError(error.EndOfStream, source.takeByte());
|
||||
terminal_screen.pages.assertIntegrity();
|
||||
terminal_screen.assertIntegrity();
|
||||
}
|
||||
|
||||
// Routing and sequence length still determine which native screen is
|
||||
// mutated and where the following record begins, so they remain strict.
|
||||
const Case = struct {
|
||||
@@ -745,8 +656,6 @@ test "HISTORY accepts row metadata mismatches and rejects structural ones" {
|
||||
.header = .{
|
||||
.key = .alternate,
|
||||
.page_count = 0,
|
||||
.total_rows = 0,
|
||||
.screen_overlap_rows = 0,
|
||||
},
|
||||
.expected = error.UnexpectedScreenKey,
|
||||
},
|
||||
@@ -754,8 +663,6 @@ test "HISTORY accepts row metadata mismatches and rejects structural ones" {
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
.page_count = 1,
|
||||
.total_rows = 1,
|
||||
.screen_overlap_rows = 0,
|
||||
},
|
||||
.expected = error.EndOfStream,
|
||||
},
|
||||
|
||||
@@ -74,7 +74,9 @@
|
||||
//! READY and FINISH contain BLAKE3-256 digests of all preceding snapshot bytes.
|
||||
//! READY therefore validates the renderable active-state prefix. FINISH covers
|
||||
//! READY and all history as well, validating the complete snapshot and its
|
||||
//! record ordering.
|
||||
//! record ordering. Each SCREEN declares its complete logical history extent,
|
||||
//! allowing a client to size its scrollbar at READY even though older PAGE
|
||||
//! records arrive afterward.
|
||||
//!
|
||||
//! ## Encoding
|
||||
//!
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
//!
|
||||
//! The final screen-height rows are the active area. When its boundary falls
|
||||
//! inside the first encoded page, that page also contains an incidental history
|
||||
//! prefix. A client may expose or ignore those resident rows, but must not
|
||||
//! require them or infer the complete history extent from them.
|
||||
//! prefix. `history_rows` declares the complete logical history extent so a
|
||||
//! client can size its scrollbar at READY without waiting for HISTORY. A client
|
||||
//! may expose or ignore the resident overlap carried by SCREEN. The extent is
|
||||
//! advisory; native row totals remain derived from decoded PAGE records.
|
||||
//!
|
||||
//! Screen dimensions are terminal-wide state and are not repeated here. Page
|
||||
//! capacities, native page IDs and pointers, authoritative history extent and
|
||||
//! availability, scrollbar state, selection, viewport, dirty state, and derived
|
||||
//! semantic-prompt state are also omitted.
|
||||
//! capacities, native page IDs and pointers, history availability, selection,
|
||||
//! viewport, dirty state, and derived semantic-prompt state are also omitted.
|
||||
//!
|
||||
//! All integers are unsigned and little-endian.
|
||||
//!
|
||||
@@ -37,10 +38,9 @@
|
||||
//! n = page_count
|
||||
//! ```
|
||||
//!
|
||||
//! 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 HISTORY record for this screen declares and sends the older complete
|
||||
//! pages after the terminal becomes ready. The incidental prefix is already
|
||||
//! present in SCREEN and is not sent again.
|
||||
//!
|
||||
//! The SCREEN payload begins with a fixed header. When the header says there is
|
||||
//! no saved cursor, the payload is:
|
||||
@@ -48,7 +48,7 @@
|
||||
//! ```text
|
||||
//! 0 +----------------------+
|
||||
//! | Header |
|
||||
//! 45 +----------------------+
|
||||
//! 53 +----------------------+
|
||||
//! | Cursor hyperlink |
|
||||
//! | variable |
|
||||
//! end +----------------------+
|
||||
@@ -59,9 +59,9 @@
|
||||
//! ```text
|
||||
//! 0 +----------------------+
|
||||
//! | Header |
|
||||
//! 45 +----------------------+
|
||||
//! 53 +----------------------+
|
||||
//! | Saved cursor |
|
||||
//! 68 +----------------------+
|
||||
//! 76 +----------------------+
|
||||
//! | Cursor hyperlink |
|
||||
//! | variable |
|
||||
//! end +----------------------+
|
||||
@@ -79,33 +79,35 @@
|
||||
//! 2 +----------------------------------+
|
||||
//! | Page count (u16) |
|
||||
//! 4 +----------------------------------+
|
||||
//! | Logical history rows (u64) |
|
||||
//! 12 +----------------------------------+
|
||||
//! | Cursor x (u16) |
|
||||
//! 6 +----------------------------------+
|
||||
//! 14 +----------------------------------+
|
||||
//! | Cursor y (u16) |
|
||||
//! 8 +----------------------------------+
|
||||
//! 16 +----------------------------------+
|
||||
//! | Cursor visual style (u8) |
|
||||
//! 9 +----------------------------------+
|
||||
//! 17 +----------------------------------+
|
||||
//! | Cursor flags (u8) |
|
||||
//! 10 +----------------------------------+
|
||||
//! 18 +----------------------------------+
|
||||
//! | Concrete cursor pen (Style) |
|
||||
//! 26 +----------------------------------+
|
||||
//! | Hyperlink implicit counter (u32) |
|
||||
//! 30 +----------------------------------+
|
||||
//! | Current charset (CharsetState) |
|
||||
//! 32 +----------------------------------+
|
||||
//! | Protected mode (u8) |
|
||||
//! 33 +----------------------------------+
|
||||
//! | Kitty keyboard stack index (u8) |
|
||||
//! 34 +----------------------------------+
|
||||
//! | Hyperlink implicit counter (u32) |
|
||||
//! 38 +----------------------------------+
|
||||
//! | Current charset (CharsetState) |
|
||||
//! 40 +----------------------------------+
|
||||
//! | Protected mode (u8) |
|
||||
//! 41 +----------------------------------+
|
||||
//! | Kitty keyboard stack index (u8) |
|
||||
//! 42 +----------------------------------+
|
||||
//! | Kitty keyboard stack flags |
|
||||
//! | 8 * u8 |
|
||||
//! 42 +----------------------------------+
|
||||
//! 50 +----------------------------------+
|
||||
//! | Semantic-click kind (u8) |
|
||||
//! 43 +----------------------------------+
|
||||
//! 51 +----------------------------------+
|
||||
//! | Semantic-click value (u8) |
|
||||
//! 44 +----------------------------------+
|
||||
//! 52 +----------------------------------+
|
||||
//! | Saved-cursor-present (u8) |
|
||||
//! 45 +----------------------------------+
|
||||
//! 53 +----------------------------------+
|
||||
//! ```
|
||||
//!
|
||||
//! The concrete cursor pen uses the fixed style encoding from `style.zig`.
|
||||
@@ -298,6 +300,8 @@ pub const DecodeError = PayloadDecodeError ||
|
||||
/// One decoded SCREEN sequence and the native screen identified by its header.
|
||||
pub const Decoded = struct {
|
||||
key: TerminalScreenKey,
|
||||
/// Expected history extent at READY, including resident SCREEN overlap.
|
||||
history_rows: u64,
|
||||
screen: TerminalScreen,
|
||||
|
||||
/// Release the decoded native screen before ownership is transferred.
|
||||
@@ -507,7 +511,11 @@ pub fn decode(
|
||||
// before transferring ownership to the caller.
|
||||
result.pages.assertIntegrity();
|
||||
result.assertIntegrity();
|
||||
return .{ .key = key, .screen = result };
|
||||
return .{
|
||||
.key = key,
|
||||
.history_rows = header.history_rows,
|
||||
.screen = result,
|
||||
};
|
||||
}
|
||||
|
||||
/// Errors possible while decoding fixed SCREEN payload fields.
|
||||
@@ -868,12 +876,15 @@ pub const Header = struct {
|
||||
pub const len = computeLen();
|
||||
|
||||
comptime {
|
||||
std.debug.assert(len == 45);
|
||||
std.debug.assert(len == 53);
|
||||
}
|
||||
|
||||
key: TerminalScreenKey,
|
||||
/// Complete pages in the minimal suffix covering the active area.
|
||||
page_count: u16,
|
||||
/// Advisory logical rows before the active area, including resident
|
||||
/// SCREEN overlap.
|
||||
history_rows: u64,
|
||||
cursor_x: u16,
|
||||
cursor_y: u16,
|
||||
cursor_style: TerminalScreen.CursorStyle,
|
||||
@@ -895,6 +906,9 @@ pub const Header = struct {
|
||||
return .{
|
||||
.key = key,
|
||||
.page_count = page_count,
|
||||
.history_rows = @intCast(
|
||||
screen.pages.total_rows - screen.pages.rows,
|
||||
),
|
||||
.cursor_x = screen.cursor.x,
|
||||
.cursor_y = screen.cursor.y,
|
||||
.cursor_style = screen.cursor.cursor_style,
|
||||
@@ -932,9 +946,10 @@ pub const Header = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Screen identity and cursor position.
|
||||
// Screen identity, expected history extent, and cursor position.
|
||||
try io.writeInt(writer, u16, @intCast(@intFromEnum(self.key)));
|
||||
try io.writeInt(writer, u16, self.page_count);
|
||||
try io.writeInt(writer, u64, self.history_rows);
|
||||
try io.writeInt(writer, u16, self.cursor_x);
|
||||
try io.writeInt(writer, u16, self.cursor_y);
|
||||
|
||||
@@ -975,12 +990,13 @@ pub const Header = struct {
|
||||
/// The screen key remains structural because it routes the following PAGE
|
||||
/// sequence. Unknown semantic fields use native defaults instead.
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!Header {
|
||||
// Screen identity and cursor position.
|
||||
// Screen identity, expected history extent, and cursor position.
|
||||
const key = enumFromInt(
|
||||
TerminalScreenKey,
|
||||
try io.readInt(reader, u16),
|
||||
) orelse return error.InvalidKey;
|
||||
const page_count = try io.readInt(reader, u16);
|
||||
const history_rows = try io.readInt(reader, u64);
|
||||
const cursor_x = try io.readInt(reader, u16);
|
||||
const cursor_y = try io.readInt(reader, u16);
|
||||
|
||||
@@ -1012,6 +1028,7 @@ pub const Header = struct {
|
||||
return .{
|
||||
.key = key,
|
||||
.page_count = page_count,
|
||||
.history_rows = history_rows,
|
||||
.cursor_x = cursor_x,
|
||||
.cursor_y = cursor_y,
|
||||
|
||||
@@ -1037,6 +1054,7 @@ pub const Header = struct {
|
||||
const value: Header = .{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.history_rows = 0,
|
||||
.cursor_x = 0,
|
||||
.cursor_y = 0,
|
||||
.cursor_style = .bar,
|
||||
@@ -1135,6 +1153,7 @@ fn testHeader() Header {
|
||||
return .{
|
||||
.key = .alternate,
|
||||
.page_count = 0x0203,
|
||||
.history_rows = 0x0102030405060708,
|
||||
.cursor_x = 0x0405,
|
||||
.cursor_y = 0x0607,
|
||||
.cursor_style = .block_hollow,
|
||||
@@ -1444,16 +1463,16 @@ test "header decoding rejects structural values" {
|
||||
test "header decoding normalizes unknown semantic values" {
|
||||
var fixture = [_]u8{0} ** Header.len;
|
||||
|
||||
fixture[8] = 4; // Unknown cursor style.
|
||||
fixture[9] = 0xFF; // Known flags, unknown semantic value, reserved bits.
|
||||
fixture[10] = 3; // Unknown cursor foreground color kind.
|
||||
fixture[31] = 0xD0; // Invalid single shift plus a reserved bit.
|
||||
fixture[32] = 3; // Unknown protected mode.
|
||||
fixture[33] = 8; // Out-of-range Kitty keyboard index.
|
||||
@memset(fixture[34..42], 0xFF); // Known flags plus reserved bits.
|
||||
fixture[42] = 3; // Unknown semantic-click kind.
|
||||
fixture[43] = 0xFF;
|
||||
fixture[44] = 2; // Noncanonical true.
|
||||
fixture[16] = 4; // Unknown cursor style.
|
||||
fixture[17] = 0xFF; // Known flags, unknown semantic value, reserved bits.
|
||||
fixture[18] = 3; // Unknown cursor foreground color kind.
|
||||
fixture[39] = 0xD0; // Invalid single shift plus a reserved bit.
|
||||
fixture[40] = 3; // Unknown protected mode.
|
||||
fixture[41] = 8; // Out-of-range Kitty keyboard index.
|
||||
@memset(fixture[42..50], 0xFF); // Known flags plus reserved bits.
|
||||
fixture[50] = 3; // Unknown semantic-click kind.
|
||||
fixture[51] = 0xFF;
|
||||
fixture[52] = 2; // Noncanonical true.
|
||||
|
||||
var reader: std.Io.Reader = .fixed(&fixture);
|
||||
const decoded = try Header.decode(&reader);
|
||||
@@ -1504,8 +1523,8 @@ test "header decoding normalizes unknown semantic values" {
|
||||
};
|
||||
for (invalid_semantic_clicks) |invalid| {
|
||||
var semantic_fixture = [_]u8{0} ** Header.len;
|
||||
semantic_fixture[42] = invalid[0];
|
||||
semantic_fixture[43] = invalid[1];
|
||||
semantic_fixture[50] = invalid[0];
|
||||
semantic_fixture[51] = invalid[1];
|
||||
var semantic_reader: std.Io.Reader = .fixed(&semantic_fixture);
|
||||
const semantic_decoded = try Header.decode(&semantic_reader);
|
||||
try std.testing.expectEqual(
|
||||
@@ -1541,6 +1560,7 @@ test "native SCREEN payload omits absent optional state" {
|
||||
const header = try Header.decode(&reader);
|
||||
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
|
||||
try std.testing.expectEqual(@as(u16, 1), header.page_count);
|
||||
try std.testing.expectEqual(@as(u64, 0), header.history_rows);
|
||||
try std.testing.expect(!header.saved_cursor_present);
|
||||
try std.testing.expectEqual(
|
||||
null,
|
||||
@@ -1867,6 +1887,10 @@ test "SCREEN encodes the minimal complete-page active suffix" {
|
||||
const header = try Header.decode(payload_reader);
|
||||
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
|
||||
try std.testing.expectEqual(@as(u16, 2), header.page_count);
|
||||
try std.testing.expectEqual(
|
||||
@as(u64, @intCast(screen.pages.total_rows - screen.pages.rows)),
|
||||
header.history_rows,
|
||||
);
|
||||
try std.testing.expect(!header.saved_cursor_present);
|
||||
try std.testing.expectEqual(
|
||||
null,
|
||||
@@ -1904,11 +1928,22 @@ test "SCREEN encodes the minimal complete-page active suffix" {
|
||||
);
|
||||
defer decoded.deinit();
|
||||
try std.testing.expectEqual(TerminalScreenKey.primary, decoded.key);
|
||||
try std.testing.expectEqual(header.history_rows, decoded.history_rows);
|
||||
const restored = &decoded.screen;
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), restored.pages.totalPages());
|
||||
const restored_top = restored.pages.getTopLeft(.active);
|
||||
try std.testing.expectEqual(@as(u16, 1), restored_top.y);
|
||||
try std.testing.expectEqual(
|
||||
@as(usize, restored_top.y),
|
||||
restored.pages.total_rows - restored.pages.rows,
|
||||
);
|
||||
try std.testing.expect(
|
||||
decoded.history_rows > @as(
|
||||
u64,
|
||||
@intCast(restored.pages.total_rows - restored.pages.rows),
|
||||
),
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
@as(u21, 'B'),
|
||||
restored.pages.getTopLeft(.screen).node
|
||||
|
||||
@@ -11,7 +11,8 @@ doc: |
|
||||
renderable screen sequences, a READY checkpoint, matching history sequences,
|
||||
and a FINISH checkpoint. SCREEN pages are oldest-to-newest. HISTORY pages are
|
||||
newest-to-oldest. FINISH terminates the snapshot; bytes that follow belong to
|
||||
the containing transport and are outside this schema.
|
||||
the containing transport and are outside this schema. Each SCREEN declares
|
||||
its complete logical history extent before READY.
|
||||
|
||||
Record CRC32C values and checkpoint BLAKE3-256 digests are represented here
|
||||
but cannot be calculated by portable Kaitai Struct expressions. The adjacent
|
||||
@@ -568,6 +569,8 @@ types:
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: history_rows
|
||||
type: u8
|
||||
- id: cursor_x
|
||||
type: u2
|
||||
- id: cursor_y
|
||||
@@ -710,10 +713,6 @@ types:
|
||||
expr: _.to_i <= 1
|
||||
- id: page_count
|
||||
type: u4
|
||||
- id: total_rows
|
||||
type: u8
|
||||
- id: screen_overlap_rows
|
||||
type: u2
|
||||
- id: trailing_data
|
||||
size-eos: true
|
||||
valid:
|
||||
|
||||
114
src/terminal/snapshot/testdata/complete-v1.hex
vendored
114
src/terminal/snapshot/testdata/complete-v1.hex
vendored
@@ -72,70 +72,68 @@ ee ee 80 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000037a
|
||||
00 63 6f 6d 70 6c 65 74 65 20 73 6e 61 70 73 68 # 0x000003ba
|
||||
6f 74 # 0x000003ca
|
||||
|
||||
# offset 0x000003cc: screen record, payload 46 bytes
|
||||
02 00 2e 00 00 00 dc 02 f1 37 00 00 01 00 00 00 # 0x000003cc
|
||||
00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003dc
|
||||
00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 # 0x000003ec
|
||||
00 00 00 00 00 00 00 00 # 0x000003fc
|
||||
# offset 0x000003cc: screen record, payload 54 bytes
|
||||
02 00 36 00 00 00 67 31 0e c6 00 00 01 00 04 00 # 0x000003cc
|
||||
00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 # 0x000003dc
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003ec
|
||||
00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003fc
|
||||
|
||||
# offset 0x00000404: page record, payload 119 bytes
|
||||
03 00 77 00 00 00 48 b7 91 cb 02 00 03 00 00 00 # 0x00000404
|
||||
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000414
|
||||
00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 00 # 0x00000424
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000434
|
||||
00 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 # 0x00000444
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000454
|
||||
00 00 00 00 00 00 00 00 00 45 00 00 00 00 00 00 # 0x00000464
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000474
|
||||
00 # 0x00000484
|
||||
# offset 0x0000040c: page record, payload 119 bytes
|
||||
03 00 77 00 00 00 48 b7 91 cb 02 00 03 00 00 00 # 0x0000040c
|
||||
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x0000041c
|
||||
00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 00 # 0x0000042c
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000043c
|
||||
00 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 # 0x0000044c
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000045c
|
||||
00 00 00 00 00 00 00 00 00 45 00 00 00 00 00 00 # 0x0000046c
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000047c
|
||||
00 # 0x0000048c
|
||||
|
||||
# offset 0x00000485: screen record, payload 46 bytes
|
||||
02 00 2e 00 00 00 05 b9 5a 3a 01 00 01 00 01 00 # 0x00000485
|
||||
02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000495
|
||||
00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 # 0x000004a5
|
||||
00 00 00 00 00 00 00 00 # 0x000004b5
|
||||
# offset 0x0000048d: screen record, payload 54 bytes
|
||||
02 00 36 00 00 00 ce 1f 9f 08 01 00 01 00 00 00 # 0x0000048d
|
||||
00 00 00 00 00 00 01 00 02 00 01 00 00 00 00 00 # 0x0000049d
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000004ad
|
||||
00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000004bd
|
||||
|
||||
# offset 0x000004bd: page record, payload 119 bytes
|
||||
03 00 77 00 00 00 84 a6 e9 08 02 00 03 00 00 00 # 0x000004bd
|
||||
00 00 80 00 c0 00 00 02 00 00 00 08 00 00 03 00 # 0x000004cd
|
||||
00 00 00 00 00 00 00 72 00 00 00 00 00 00 00 00 # 0x000004dd
|
||||
00 00 00 00 00 00 00 6e 00 00 00 00 00 00 00 03 # 0x000004ed
|
||||
00 00 00 00 00 00 00 00 61 00 00 00 00 00 00 00 # 0x000004fd
|
||||
00 00 00 00 00 00 00 00 74 00 00 00 00 00 00 00 # 0x0000050d
|
||||
03 00 00 00 00 00 00 00 00 65 00 00 00 00 00 00 # 0x0000051d
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000052d
|
||||
00 # 0x0000053d
|
||||
# offset 0x000004cd: page record, payload 119 bytes
|
||||
03 00 77 00 00 00 84 a6 e9 08 02 00 03 00 00 00 # 0x000004cd
|
||||
00 00 80 00 c0 00 00 02 00 00 00 08 00 00 03 00 # 0x000004dd
|
||||
00 00 00 00 00 00 00 72 00 00 00 00 00 00 00 00 # 0x000004ed
|
||||
00 00 00 00 00 00 00 6e 00 00 00 00 00 00 00 03 # 0x000004fd
|
||||
00 00 00 00 00 00 00 00 61 00 00 00 00 00 00 00 # 0x0000050d
|
||||
00 00 00 00 00 00 00 00 74 00 00 00 00 00 00 00 # 0x0000051d
|
||||
03 00 00 00 00 00 00 00 00 65 00 00 00 00 00 00 # 0x0000052d
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000053d
|
||||
00 # 0x0000054d
|
||||
|
||||
# offset 0x0000053e: ready record, payload 32 bytes
|
||||
05 00 20 00 00 00 29 ce 51 e1 28 ab 80 18 19 00 # 0x0000053e
|
||||
ef b3 15 fa 9d 03 c3 42 7f 77 2d f3 0c 00 87 cc # 0x0000054e
|
||||
a1 dc ac ce 6c fc fb 27 66 e1 # 0x0000055e
|
||||
# offset 0x0000054e: ready record, payload 32 bytes
|
||||
05 00 20 00 00 00 43 0d 7b 3a 84 06 56 ba 02 59 # 0x0000054e
|
||||
04 24 20 59 96 92 14 ba 76 53 80 40 09 12 85 a7 # 0x0000055e
|
||||
f2 b5 b8 f9 63 98 e8 dc 9a 39 # 0x0000056e
|
||||
|
||||
# offset 0x00000568: history record, payload 16 bytes
|
||||
04 00 10 00 00 00 cc 85 a3 b6 00 00 02 00 00 00 # 0x00000568
|
||||
04 00 00 00 00 00 00 00 00 00 # 0x00000578
|
||||
# offset 0x00000578: history record, payload 6 bytes
|
||||
04 00 06 00 00 00 20 32 ed e1 00 00 02 00 00 00 # 0x00000578
|
||||
|
||||
# offset 0x00000582: page record, payload 86 bytes
|
||||
03 00 56 00 00 00 52 3b 6e 8d 02 00 02 00 00 00 # 0x00000582
|
||||
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000592
|
||||
00 00 00 00 00 00 00 42 00 00 00 00 00 00 00 00 # 0x000005a2
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005b2
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005c2
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005d2
|
||||
# offset 0x00000588: page record, payload 86 bytes
|
||||
03 00 56 00 00 00 52 3b 6e 8d 02 00 02 00 00 00 # 0x00000588
|
||||
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000598
|
||||
00 00 00 00 00 00 00 42 00 00 00 00 00 00 00 00 # 0x000005a8
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005b8
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005c8
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005d8
|
||||
|
||||
# offset 0x000005e2: page record, payload 86 bytes
|
||||
03 00 56 00 00 00 fb bc 15 06 02 00 02 00 00 00 # 0x000005e2
|
||||
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x000005f2
|
||||
00 00 00 00 00 00 00 41 00 00 00 00 00 00 00 00 # 0x00000602
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000612
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000622
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000632
|
||||
# offset 0x000005e8: page record, payload 86 bytes
|
||||
03 00 56 00 00 00 fb bc 15 06 02 00 02 00 00 00 # 0x000005e8
|
||||
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x000005f8
|
||||
00 00 00 00 00 00 00 41 00 00 00 00 00 00 00 00 # 0x00000608
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000618
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000628
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000638
|
||||
|
||||
# offset 0x00000642: history record, payload 16 bytes
|
||||
04 00 10 00 00 00 39 57 cc cf 01 00 00 00 00 00 # 0x00000642
|
||||
00 00 00 00 00 00 00 00 00 00 # 0x00000652
|
||||
# offset 0x00000648: history record, payload 6 bytes
|
||||
04 00 06 00 00 00 b8 7a ba b1 01 00 00 00 00 00 # 0x00000648
|
||||
|
||||
# offset 0x0000065c: finish record, payload 32 bytes
|
||||
06 00 20 00 00 00 05 71 15 22 3e 4d e9 7a c1 02 # 0x0000065c
|
||||
0b a3 6a 10 a0 88 1c 07 03 2c 68 52 15 be 76 d0 # 0x0000066c
|
||||
4c a9 a6 2b fd f4 ac 49 62 2b # 0x0000067c
|
||||
# offset 0x00000658: finish record, payload 32 bytes
|
||||
06 00 20 00 00 00 8d d6 46 7a ac 02 c8 09 9d 1a # 0x00000658
|
||||
5a 00 ce 47 5e d7 b0 ae 54 10 bb 0d f1 35 7c ab # 0x00000668
|
||||
92 fd ae d3 5e 1a 77 1e 40 7f # 0x00000678
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
# offset 0x00000000: encoded bytes
|
||||
01 00 04 03 02 01 08 07 06 05 04 03 02 01 0a 09 # 0x00000000
|
||||
01 00 04 03 02 01 # 0x00000000
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
# offset 0x00000000: encoded bytes
|
||||
01 00 03 02 05 04 07 06 03 19 00 00 00 00 01 7f # 0x00000000
|
||||
00 00 02 12 34 56 ff 03 00 00 0d 0c 0b 0a e4 3d # 0x00000010
|
||||
02 07 01 02 04 08 10 1f 00 11 02 03 01 # 0x00000020
|
||||
01 00 03 02 08 07 06 05 04 03 02 01 05 04 07 06 # 0x00000000
|
||||
03 19 00 00 00 00 01 7f 00 00 02 12 34 56 ff 03 # 0x00000010
|
||||
00 00 0d 0c 0b 0a e4 3d 02 07 01 02 04 08 10 1f # 0x00000020
|
||||
00 11 02 03 01 # 0x00000030
|
||||
|
||||
@@ -256,19 +256,14 @@ def validate_complete_snapshot(snapshot: Any, data: bytes) -> None:
|
||||
"the active area"
|
||||
)
|
||||
overlap_rows = screen_rows - terminal_header.rows
|
||||
if payload.screen_overlap_rows != overlap_rows:
|
||||
raise ValueError(
|
||||
f"HISTORY key {payload.key}: screen_overlap_rows "
|
||||
f"{payload.screen_overlap_rows} does not match {overlap_rows}"
|
||||
)
|
||||
|
||||
total_rows = payload.screen_overlap_rows + sum(
|
||||
history_rows = overlap_rows + sum(
|
||||
page.payload.header.rows for page in sequence.pages
|
||||
)
|
||||
if total_rows != payload.total_rows:
|
||||
if history_rows != screen.screen.payload.header.history_rows:
|
||||
raise ValueError(
|
||||
f"HISTORY key {payload.key}: total_rows "
|
||||
f"{payload.total_rows} does not match {total_rows}"
|
||||
f"SCREEN key {payload.key}: history_rows "
|
||||
f"{screen.screen.payload.header.history_rows} "
|
||||
f"does not match {history_rows}"
|
||||
)
|
||||
|
||||
for record in all_snapshot_records(snapshot):
|
||||
|
||||
Reference in New Issue
Block a user