mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 20:59:02 +00:00
terminal/snapshot: screen decoding
This commit is contained in:
@@ -144,6 +144,9 @@ const PayloadDecodeError = style.DecodeError ||
|
||||
|
||||
/// Native page backing memory could not be allocated.
|
||||
OutOfMemory,
|
||||
|
||||
/// The caller-provided page does not have the advertised capacity.
|
||||
InvalidDestinationCapacity,
|
||||
};
|
||||
|
||||
/// Errors possible while encoding a complete PAGE record.
|
||||
@@ -183,17 +186,70 @@ pub fn decode(
|
||||
reader: *std.Io.Reader,
|
||||
alloc: Allocator,
|
||||
) DecodeError!TerminalPage {
|
||||
var record_reader: record.Reader = undefined;
|
||||
try record_reader.init(reader);
|
||||
if (record_reader.header.tag != .page) return error.UnexpectedRecordTag;
|
||||
var decoder: Decoder = undefined;
|
||||
try decoder.init(reader);
|
||||
|
||||
var page = try decodePayload(record_reader.payloadReader(), alloc);
|
||||
var page = TerminalPage.init(decoder.capacity()) catch
|
||||
return error.OutOfMemory;
|
||||
errdefer page.deinit();
|
||||
try record_reader.finish();
|
||||
try page.verifyIntegrity(alloc);
|
||||
try decoder.decode(&page, alloc);
|
||||
return page;
|
||||
}
|
||||
|
||||
/// PAGE record decoder where the caller is responsible for owning
|
||||
/// the Page memory. This is particularly useful paired with
|
||||
/// PageList.Builder so you can build up a proper PageList with
|
||||
/// valid memory ownership.
|
||||
pub const Decoder = struct {
|
||||
record_reader: record.Reader,
|
||||
header: Header,
|
||||
|
||||
/// Begin decoding one PAGE record and expose its required capacity.
|
||||
/// After this, call `capacity` to get the capacity to allocate
|
||||
/// the page properly, then `decode` into it.
|
||||
pub fn init(self: *Decoder, reader: *std.Io.Reader) DecodeError!void {
|
||||
try self.record_reader.init(reader);
|
||||
if (self.record_reader.header.tag != .page) {
|
||||
return error.UnexpectedRecordTag;
|
||||
}
|
||||
|
||||
self.header = try Header.decode(self.record_reader.payloadReader());
|
||||
_ = try self.header.pageCapacity();
|
||||
}
|
||||
|
||||
/// Return the exact native capacity advertised by the PAGE header.
|
||||
pub fn capacity(self: *const Decoder) TerminalPageCapacity {
|
||||
return self.header.pageCapacity() catch unreachable;
|
||||
}
|
||||
|
||||
/// Decode the remaining payload into caller-owned native page storage.
|
||||
///
|
||||
/// The destination must be freshly initialized with `capacity`. `alloc`
|
||||
/// is used only for temporary ID remaps and integrity-check storage.
|
||||
pub fn decode(
|
||||
self: *Decoder,
|
||||
destination: *TerminalPage,
|
||||
alloc: Allocator,
|
||||
) DecodeError!void {
|
||||
if (!std.meta.eql(destination.capacity, self.capacity())) {
|
||||
return error.InvalidDestinationCapacity;
|
||||
}
|
||||
|
||||
destination.size = .{
|
||||
.cols = self.header.columns,
|
||||
.rows = self.header.rows,
|
||||
};
|
||||
try decodePayloadBody(
|
||||
self.record_reader.payloadReader(),
|
||||
alloc,
|
||||
destination,
|
||||
self.header,
|
||||
);
|
||||
try self.record_reader.finish();
|
||||
try destination.verifyIntegrity(alloc);
|
||||
}
|
||||
};
|
||||
|
||||
/// Encode a PAGE payload directly from a native page.
|
||||
fn encodePayload(
|
||||
page: *const TerminalPage,
|
||||
@@ -228,12 +284,23 @@ fn decodePayload(
|
||||
reader: *std.Io.Reader,
|
||||
alloc: Allocator,
|
||||
) PayloadDecodeError!TerminalPage {
|
||||
// Decode the header, validate capacities, init page
|
||||
const header = try Header.decode(reader);
|
||||
const capacity = try header.pageCapacity();
|
||||
var page = TerminalPage.init(capacity) catch
|
||||
return error.OutOfMemory;
|
||||
errdefer page.deinit();
|
||||
try decodePayloadBody(reader, alloc, &page, header);
|
||||
return page;
|
||||
}
|
||||
|
||||
/// Decode PAGE tables and grid after the header into initialized native page
|
||||
/// storage with the exact advertised capacity and dimensions.
|
||||
fn decodePayloadBody(
|
||||
reader: *std.Io.Reader,
|
||||
alloc: Allocator,
|
||||
page: *TerminalPage,
|
||||
header: Header,
|
||||
) PayloadDecodeError!void {
|
||||
page.pauseIntegrityChecks(true);
|
||||
defer page.pauseIntegrityChecks(false);
|
||||
|
||||
@@ -293,7 +360,7 @@ fn decodePayload(
|
||||
}
|
||||
|
||||
const decoded_id = hyperlink.decodePage(
|
||||
&page,
|
||||
page,
|
||||
reader,
|
||||
) catch |err| switch (err) {
|
||||
error.StringsOutOfMemory => return error.InvalidStringCapacity,
|
||||
@@ -314,13 +381,11 @@ fn decodePayload(
|
||||
|
||||
// Rows and cells
|
||||
try grid.decode(
|
||||
&page,
|
||||
page,
|
||||
reader,
|
||||
&style_remap,
|
||||
&hyperlink_remap,
|
||||
);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/// The fixed logical dimensions, table counts, and allocation hints at the
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
//! bump.
|
||||
|
||||
const std = @import("std");
|
||||
const build_options = @import("terminal_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const hyperlink = @import("hyperlink.zig");
|
||||
const io = @import("io.zig");
|
||||
@@ -207,6 +208,8 @@ const terminal_ansi = @import("../ansi.zig");
|
||||
const terminal_charsets = @import("../charsets.zig");
|
||||
const terminal_hyperlink = @import("../hyperlink.zig");
|
||||
const terminal_kitty = @import("../kitty.zig");
|
||||
const terminal_page = @import("../page.zig");
|
||||
const TerminalPageList = @import("../PageList.zig");
|
||||
const TerminalScreen = @import("../Screen.zig");
|
||||
const TerminalScreenKey = @import("../ScreenSet.zig").Key;
|
||||
const terminal_style = @import("../style.zig");
|
||||
@@ -278,8 +281,233 @@ pub fn encode(
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors possible while restoring a SCREEN and its declared PAGE sequence.
|
||||
pub const DecodeError = PayloadDecodeError ||
|
||||
hyperlink.DecodeError ||
|
||||
page.DecodeError ||
|
||||
record.Reader.InitError ||
|
||||
record.Reader.FinishError ||
|
||||
TerminalPageList.Builder.FinishError ||
|
||||
TerminalPageList.IncreaseCapacityError ||
|
||||
error{
|
||||
/// The next record is valid but is not a SCREEN.
|
||||
UnexpectedRecordTag,
|
||||
|
||||
/// The SCREEN key does not match the caller-selected native screen.
|
||||
UnexpectedScreenKey,
|
||||
|
||||
/// A SCREEN must declare at least one PAGE.
|
||||
InvalidPageCount,
|
||||
|
||||
/// The cursor is outside the restored active area.
|
||||
InvalidCursorPosition,
|
||||
};
|
||||
|
||||
/// Restore one SCREEN and its declared PAGE records into native terminal state.
|
||||
///
|
||||
/// The caller supplies terminal-wide dimensions and policy. The record sequence
|
||||
/// is decoded transactionally: on failure, all page, pin, hyperlink, and
|
||||
/// temporary allocations are released and no partially restored Screen escapes.
|
||||
pub fn decode(
|
||||
source: *std.Io.Reader,
|
||||
io_: std.Io,
|
||||
alloc: Allocator,
|
||||
expected_key: TerminalScreenKey,
|
||||
options: TerminalScreen.Options,
|
||||
) DecodeError!TerminalScreen {
|
||||
// Decode and finish the self-contained SCREEN record before consuming any
|
||||
// of the PAGE records which follow it.
|
||||
var record_reader: record.Reader = undefined;
|
||||
try record_reader.init(source);
|
||||
if (record_reader.header.tag != .screen) {
|
||||
return error.UnexpectedRecordTag;
|
||||
}
|
||||
|
||||
// The optional saved cursor and cursor hyperlink are both part of the
|
||||
// SCREEN payload. Keep ownership of the decoded hyperlink locally until it
|
||||
// has been copied into the native Screen.
|
||||
const payload_reader = record_reader.payloadReader();
|
||||
const header = try Header.decode(payload_reader);
|
||||
const saved_cursor = if (header.saved_cursor_present)
|
||||
try SavedCursor.decode(payload_reader)
|
||||
else
|
||||
null;
|
||||
var cursor_hyperlink = try decodeCursorHyperlink(
|
||||
payload_reader,
|
||||
alloc,
|
||||
);
|
||||
defer if (cursor_hyperlink) |*value| value.deinit(alloc);
|
||||
try record_reader.finish();
|
||||
|
||||
// Reject a structurally valid SCREEN which does not describe the native
|
||||
// screen selected by the caller.
|
||||
if (header.key.terminal() != expected_key) {
|
||||
return error.UnexpectedScreenKey;
|
||||
}
|
||||
if (header.page_count == 0) return error.InvalidPageCount;
|
||||
|
||||
// Dimensions are terminal-wide state supplied by the enclosing snapshot.
|
||||
// Validate them, and all cursor invariants which depend on them, before
|
||||
// allocating the declared pages.
|
||||
if (options.cols == 0 or options.rows == 0) {
|
||||
return error.InvalidDimensions;
|
||||
}
|
||||
if (header.cursor_x >= options.cols or
|
||||
header.cursor_y >= options.rows or
|
||||
(header.cursor_flags.pending_wrap and
|
||||
header.cursor_x != options.cols - 1))
|
||||
{
|
||||
return error.InvalidCursorPosition;
|
||||
}
|
||||
|
||||
// Build the native page list transactionally. Alternate screens never
|
||||
// retain scrollback, while primary screens inherit the caller's limits.
|
||||
var result: TerminalScreen = result: {
|
||||
var builder = try TerminalPageList.Builder.init(alloc, .{
|
||||
.cols = options.cols,
|
||||
.rows = options.rows,
|
||||
.max_size = if (expected_key == .alternate)
|
||||
0
|
||||
else
|
||||
options.max_scrollback_bytes,
|
||||
.max_lines = if (expected_key == .alternate)
|
||||
0
|
||||
else
|
||||
options.max_scrollback_lines,
|
||||
});
|
||||
defer builder.deinit();
|
||||
|
||||
// Allocate each PAGE at its declared capacity and decode directly into
|
||||
// builder-owned storage, avoiding an intermediate page representation.
|
||||
for (0..header.page_count) |_| {
|
||||
var decoder: page.Decoder = undefined;
|
||||
try decoder.init(source);
|
||||
const native_page = try builder.allocatePage(decoder.capacity());
|
||||
try decoder.decode(native_page, alloc);
|
||||
}
|
||||
|
||||
// Finishing validates that the decoded suffix covers the active area
|
||||
// and establishes the PageList's active viewport.
|
||||
var pages = try builder.finish();
|
||||
errdefer pages.deinit();
|
||||
|
||||
// Convert the payload-relative cursor position into the tracked native
|
||||
// pin and page-local row/cell pointers required by TerminalScreen.
|
||||
const cursor_pin_value = pages.pin(.{ .active = .{
|
||||
.x = header.cursor_x,
|
||||
.y = header.cursor_y,
|
||||
} }) orelse return error.InvalidCursorPosition;
|
||||
const cursor_pin = try pages.trackPin(cursor_pin_value);
|
||||
const cursor_rac = cursor_pin.rowAndCell();
|
||||
|
||||
// Assemble the remaining native state from stable snapshot registries.
|
||||
// Derived state and page-local table references are repaired below.
|
||||
break :result .{
|
||||
.io = io_,
|
||||
.alloc = alloc,
|
||||
.pages = pages,
|
||||
.no_scrollback = expected_key == .alternate or
|
||||
options.max_scrollback_bytes == 0,
|
||||
.cursor = .{
|
||||
.x = header.cursor_x,
|
||||
.y = header.cursor_y,
|
||||
.cursor_style = header.cursor_style.terminal(),
|
||||
.pending_wrap = header.cursor_flags.pending_wrap,
|
||||
.protected = header.cursor_flags.protected,
|
||||
.style = header.cursor_pen,
|
||||
.hyperlink_implicit_id = header.hyperlink_implicit_id,
|
||||
.semantic_content = header.cursor_flags.semantic_content
|
||||
.terminal(),
|
||||
.semantic_content_clear_eol = header.cursor_flags
|
||||
.semantic_content_clear_eol,
|
||||
.page_pin = cursor_pin,
|
||||
.page_row = cursor_rac.row,
|
||||
.page_cell = cursor_rac.cell,
|
||||
},
|
||||
.saved_cursor = if (saved_cursor) |value|
|
||||
value.terminal()
|
||||
else
|
||||
null,
|
||||
.charset = header.charset.terminal(),
|
||||
.protected_mode = header.protected_mode.terminal(),
|
||||
.kitty_keyboard = header.kitty_keyboard.terminal(),
|
||||
.semantic_prompt = .{
|
||||
.seen = false,
|
||||
.click = header.semantic_click.terminal(),
|
||||
},
|
||||
};
|
||||
};
|
||||
errdefer result.deinit();
|
||||
|
||||
// Reinsert the concrete cursor style into its page-local style table. This
|
||||
// existing Screen path grows or splits the page when the decoded table is
|
||||
// already full.
|
||||
try result.manualStyleUpdate();
|
||||
|
||||
// Reinsert the cursor hyperlink into its page-local hyperlink table. For an
|
||||
// implicit link, temporarily seed the counter with the encoded link ID,
|
||||
// then restore the independent next-ID counter from the header.
|
||||
if (cursor_hyperlink) |value| {
|
||||
switch (value.id) {
|
||||
.explicit => |id| try result.startHyperlink(value.uri, id),
|
||||
.implicit => |id| {
|
||||
result.cursor.hyperlink_implicit_id = id;
|
||||
try result.startHyperlink(value.uri, null);
|
||||
result.cursor.hyperlink_implicit_id =
|
||||
header.hyperlink_implicit_id;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// `seen` is derived state. Only resident prompt metadata participates
|
||||
// because omitted history is outside the restored Screen model.
|
||||
result.semantic_prompt.seen = seen: {
|
||||
if (result.cursor.semantic_content == .prompt) break :seen true;
|
||||
|
||||
var node = result.pages.getTopLeft(.screen).node;
|
||||
while (true) {
|
||||
const native_page = node.pageAssumeResident();
|
||||
const rows = native_page.rows.ptr(
|
||||
native_page.memory,
|
||||
)[0..native_page.size.rows];
|
||||
for (rows) |row| {
|
||||
if (row.semantic_prompt != .none) break :seen true;
|
||||
|
||||
const cells = row.cells.ptr(
|
||||
native_page.memory,
|
||||
)[0..native_page.size.cols];
|
||||
for (cells) |cell| {
|
||||
if (cell.semantic_content == .prompt) break :seen true;
|
||||
}
|
||||
}
|
||||
|
||||
node = node.next orelse break :seen false;
|
||||
}
|
||||
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Kitty image payloads are outside this record, but the restored Screen
|
||||
// must still receive the caller's terminal-wide storage policy.
|
||||
if (comptime build_options.kitty_graphics) {
|
||||
result.kitty_images.setLimit(
|
||||
io_,
|
||||
alloc,
|
||||
&result,
|
||||
options.kitty_image_storage_limit,
|
||||
) catch unreachable;
|
||||
result.kitty_images.image_limits = options.kitty_image_loading_limits;
|
||||
}
|
||||
|
||||
// All fallible reconstruction is complete; verify the native invariants
|
||||
// before transferring ownership to the caller.
|
||||
result.pages.assertIntegrity();
|
||||
result.assertIntegrity();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Errors possible while decoding fixed SCREEN payload fields.
|
||||
pub const DecodeError = style.DecodeError || error{
|
||||
const PayloadDecodeError = style.DecodeError || error{
|
||||
InvalidKey,
|
||||
InvalidCursorStyle,
|
||||
InvalidCursorFlags,
|
||||
@@ -304,6 +532,13 @@ pub const Key = enum(u16) {
|
||||
.alternate => .alternate,
|
||||
};
|
||||
}
|
||||
|
||||
fn terminal(self: Key) TerminalScreenKey {
|
||||
return switch (self) {
|
||||
.primary => .primary,
|
||||
.alternate => .alternate,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Snapshot registry for the cursor's visual shape.
|
||||
@@ -321,6 +556,15 @@ pub const CursorStyle = enum(u8) {
|
||||
.block_hollow => .block_hollow,
|
||||
};
|
||||
}
|
||||
|
||||
fn terminal(self: CursorStyle) TerminalScreen.CursorStyle {
|
||||
return switch (self) {
|
||||
.bar => .bar,
|
||||
.block => .block,
|
||||
.underline => .underline,
|
||||
.block_hollow => .block_hollow,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Flags encoded after the cursor's visual shape.
|
||||
@@ -337,6 +581,15 @@ pub const CursorFlags = packed struct(u8) {
|
||||
input = 1,
|
||||
prompt = 2,
|
||||
invalid = 3,
|
||||
|
||||
fn terminal(self: SemanticContent) terminal_page.Cell.SemanticContent {
|
||||
return switch (self) {
|
||||
.output => .output,
|
||||
.input => .input,
|
||||
.prompt => .prompt,
|
||||
.invalid => unreachable,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn init(cursor: *const TerminalScreen.Cursor) CursorFlags {
|
||||
@@ -354,7 +607,7 @@ pub const CursorFlags = packed struct(u8) {
|
||||
}
|
||||
|
||||
/// Decode and validate the cursor flag registry.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!CursorFlags {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!CursorFlags {
|
||||
const result: CursorFlags = @bitCast(try reader.takeByte());
|
||||
if (result._padding != 0) return error.InvalidCursorFlags;
|
||||
if (result.semantic_content == .invalid) {
|
||||
@@ -381,6 +634,15 @@ pub const CharsetState = packed struct(u16) {
|
||||
ascii = 1,
|
||||
british = 2,
|
||||
dec_special = 3,
|
||||
|
||||
fn terminal(self: Charset) terminal_charsets.Charset {
|
||||
return switch (self) {
|
||||
.utf8 => .utf8,
|
||||
.ascii => .ascii,
|
||||
.british => .british,
|
||||
.dec_special => .dec_special,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Snapshot registry for a G0 through G3 charset slot.
|
||||
@@ -389,6 +651,15 @@ pub const CharsetState = packed struct(u16) {
|
||||
g1 = 1,
|
||||
g2 = 2,
|
||||
g3 = 3,
|
||||
|
||||
fn terminal(self: Slot) terminal_charsets.Slots {
|
||||
return switch (self) {
|
||||
.g0 => .G0,
|
||||
.g1 => .G1,
|
||||
.g2 => .G2,
|
||||
.g3 => .G3,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Snapshot registry for the single-shift charset slot.
|
||||
@@ -399,6 +670,17 @@ pub const CharsetState = packed struct(u16) {
|
||||
g2 = 3,
|
||||
g3 = 4,
|
||||
_,
|
||||
|
||||
fn terminal(self: SingleShift) ?terminal_charsets.Slots {
|
||||
return switch (self) {
|
||||
.none => null,
|
||||
.g0 => .G0,
|
||||
.g1 => .G1,
|
||||
.g2 => .G2,
|
||||
.g3 => .G3,
|
||||
_ => unreachable,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn init(value: TerminalScreen.CharsetState) CharsetState {
|
||||
@@ -421,6 +703,19 @@ pub const CharsetState = packed struct(u16) {
|
||||
};
|
||||
}
|
||||
|
||||
fn terminal(self: CharsetState) TerminalScreen.CharsetState {
|
||||
var result: TerminalScreen.CharsetState = .{
|
||||
.gl = self.gl.terminal(),
|
||||
.gr = self.gr.terminal(),
|
||||
.single_shift = self.single_shift.terminal(),
|
||||
};
|
||||
result.charsets.set(.G0, self.g0.terminal());
|
||||
result.charsets.set(.G1, self.g1.terminal());
|
||||
result.charsets.set(.G2, self.g2.terminal());
|
||||
result.charsets.set(.G3, self.g3.terminal());
|
||||
return result;
|
||||
}
|
||||
|
||||
fn charset(value: terminal_charsets.Charset) Charset {
|
||||
return switch (value) {
|
||||
.utf8 => .utf8,
|
||||
@@ -440,7 +735,7 @@ pub const CharsetState = packed struct(u16) {
|
||||
}
|
||||
|
||||
/// Decode and validate the packed charset registry.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!CharsetState {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!CharsetState {
|
||||
const result: CharsetState = @bitCast(try io.readInt(reader, u16));
|
||||
if (result._padding != 0) return error.InvalidCharsetState;
|
||||
switch (result.single_shift) {
|
||||
@@ -464,6 +759,14 @@ pub const ProtectedMode = enum(u8) {
|
||||
.dec => .dec,
|
||||
};
|
||||
}
|
||||
|
||||
fn terminal(self: ProtectedMode) terminal_ansi.ProtectedMode {
|
||||
return switch (self) {
|
||||
.off => .off,
|
||||
.iso => .iso,
|
||||
.dec => .dec,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The complete fixed-size Kitty keyboard stack.
|
||||
@@ -481,7 +784,7 @@ pub const KittyKeyboard = struct {
|
||||
_padding: u3 = 0,
|
||||
|
||||
/// Decode and validate one Kitty keyboard flag byte.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!Flags {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!Flags {
|
||||
const result: Flags = @bitCast(try reader.takeByte());
|
||||
if (result._padding != 0) {
|
||||
return error.InvalidKittyKeyboardFlags;
|
||||
@@ -504,8 +807,24 @@ pub const KittyKeyboard = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
fn terminal(self: KittyKeyboard) terminal_kitty.KeyFlagStack {
|
||||
var result: terminal_kitty.KeyFlagStack = .{
|
||||
.idx = @intCast(self.index),
|
||||
};
|
||||
for (self.flags, &result.flags) |snapshot, *native| {
|
||||
native.* = .{
|
||||
.disambiguate = snapshot.disambiguate,
|
||||
.report_events = snapshot.report_events,
|
||||
.report_alternates = snapshot.report_alternates,
|
||||
.report_all = snapshot.report_all,
|
||||
.report_associated = snapshot.report_associated,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Decode and validate the complete fixed Kitty keyboard stack.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!KittyKeyboard {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!KittyKeyboard {
|
||||
const index = try reader.takeByte();
|
||||
if (index >= 8) return error.InvalidKittyKeyboardIndex;
|
||||
|
||||
@@ -564,8 +883,26 @@ pub const SemanticClick = union(SemanticClickKind) {
|
||||
};
|
||||
}
|
||||
|
||||
fn terminal(
|
||||
self: SemanticClick,
|
||||
) TerminalScreen.SemanticPrompt.SemanticClick {
|
||||
return switch (self) {
|
||||
.none => .none,
|
||||
.click_events => |value| .{ .click_events = switch (value) {
|
||||
.absolute => .absolute,
|
||||
.relative => .relative,
|
||||
} },
|
||||
.cl => |value| .{ .cl = switch (value) {
|
||||
.line => .line,
|
||||
.multiple => .multiple,
|
||||
.conservative_vertical => .conservative_vertical,
|
||||
.smart_vertical => .smart_vertical,
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
/// Decode and validate the semantic-click kind and value bytes.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!SemanticClick {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!SemanticClick {
|
||||
const kind_raw = try reader.takeByte();
|
||||
const value_raw = try reader.takeByte();
|
||||
const kind = std.enums.fromInt(
|
||||
@@ -613,7 +950,7 @@ pub const SavedCursor = struct {
|
||||
_padding: u5 = 0,
|
||||
|
||||
/// Decode and validate saved cursor flags.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!Flags {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!Flags {
|
||||
const result: Flags = @bitCast(try reader.takeByte());
|
||||
if (result._padding != 0) return error.InvalidSavedCursorFlags;
|
||||
return result;
|
||||
@@ -634,6 +971,18 @@ pub const SavedCursor = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn terminal(self: SavedCursor) TerminalScreen.SavedCursor {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y,
|
||||
.style = self.pen,
|
||||
.protected = self.flags.protected,
|
||||
.pending_wrap = self.flags.pending_wrap,
|
||||
.origin = self.flags.origin,
|
||||
.charset = self.charset.terminal(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Encode one optional saved cursor value.
|
||||
pub fn encode(
|
||||
self: SavedCursor,
|
||||
@@ -656,7 +1005,7 @@ pub const SavedCursor = struct {
|
||||
}
|
||||
|
||||
/// Decode and validate one optional saved cursor value.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!SavedCursor {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!SavedCursor {
|
||||
return .{
|
||||
.x = try io.readInt(reader, u16),
|
||||
.y = try io.readInt(reader, u16),
|
||||
@@ -803,7 +1152,7 @@ pub const Header = struct {
|
||||
}
|
||||
|
||||
/// Decode and validate the fixed SCREEN payload header.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!Header {
|
||||
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!Header {
|
||||
// Screen identity and cursor position.
|
||||
const key = std.enums.fromInt(
|
||||
Key,
|
||||
@@ -1573,7 +1922,7 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
defer screen.deinit();
|
||||
|
||||
// Configure the live cursor state represented by the fixed header.
|
||||
screen.cursorAbsolute(5, 6);
|
||||
screen.cursorAbsolute(7, 6);
|
||||
screen.cursor.cursor_style = .block_hollow;
|
||||
screen.cursor.pending_wrap = true;
|
||||
screen.cursor.protected = false;
|
||||
@@ -1631,7 +1980,7 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
.origin = true,
|
||||
.charset = charset,
|
||||
};
|
||||
try screen.startHyperlink("cursor-uri", "cursor-id");
|
||||
try screen.startHyperlink("cursor-uri", null);
|
||||
|
||||
var destination: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
@@ -1653,8 +2002,9 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
const payload_reader = record_reader.payloadReader();
|
||||
var expected_header = testHeader();
|
||||
expected_header.page_count = 1;
|
||||
expected_header.cursor_x = 5;
|
||||
expected_header.cursor_x = 7;
|
||||
expected_header.cursor_y = 6;
|
||||
expected_header.hyperlink_implicit_id = 0x0a0b0c0e;
|
||||
try std.testing.expectEqualDeep(
|
||||
expected_header,
|
||||
try Header.decode(payload_reader),
|
||||
@@ -1665,7 +2015,7 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
);
|
||||
|
||||
const expected_hyperlink: TerminalHyperlink = .{
|
||||
.id = .{ .explicit = "cursor-id" },
|
||||
.id = .{ .implicit = 0x0a0b0c0d },
|
||||
.uri = "cursor-uri",
|
||||
};
|
||||
var actual_hyperlink = (try decodeCursorHyperlink(
|
||||
@@ -1681,6 +2031,87 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
try std.testing.expectEqual(@as(u16, 8), decoded_page.size.cols);
|
||||
try std.testing.expectEqual(@as(u16, 8), decoded_page.size.rows);
|
||||
try std.testing.expectError(error.EndOfStream, source.takeByte());
|
||||
|
||||
// The complete sequence restores directly into native Screen/PageList
|
||||
// state, including page-local cursor references.
|
||||
var restore_source: std.Io.Reader = .fixed(destination.written()[6..]);
|
||||
var restored = try decode(
|
||||
&restore_source,
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
.alternate,
|
||||
.{ .cols = 8, .rows = 8, .max_scrollback_bytes = 0 },
|
||||
);
|
||||
defer restored.deinit();
|
||||
|
||||
try std.testing.expect(restored.no_scrollback);
|
||||
try std.testing.expectEqual(@as(u16, 7), restored.cursor.x);
|
||||
try std.testing.expectEqual(@as(u16, 6), restored.cursor.y);
|
||||
try std.testing.expectEqual(
|
||||
TerminalScreen.CursorStyle.block_hollow,
|
||||
restored.cursor.cursor_style,
|
||||
);
|
||||
try std.testing.expect(restored.cursor.pending_wrap);
|
||||
try std.testing.expectEqual(
|
||||
terminal_page.Cell.SemanticContent.prompt,
|
||||
restored.cursor.semantic_content,
|
||||
);
|
||||
try std.testing.expect(
|
||||
restored.cursor.style.eql(testHeader().cursor_pen),
|
||||
);
|
||||
try std.testing.expect(restored.cursor.style_id != terminal_style.default_id);
|
||||
try std.testing.expectEqual(
|
||||
@as(u32, 0x0a0b0c0e),
|
||||
restored.cursor.hyperlink_implicit_id,
|
||||
);
|
||||
try expectHyperlinkEqual(
|
||||
expected_hyperlink,
|
||||
restored.cursor.hyperlink.?.*,
|
||||
);
|
||||
|
||||
const restored_saved = restored.saved_cursor.?;
|
||||
try std.testing.expectEqual(@as(u16, 0x0102), restored_saved.x);
|
||||
try std.testing.expectEqual(@as(u16, 0x0304), restored_saved.y);
|
||||
try std.testing.expect(restored_saved.protected);
|
||||
try std.testing.expect(restored_saved.pending_wrap);
|
||||
try std.testing.expect(restored_saved.origin);
|
||||
try std.testing.expectEqual(
|
||||
terminal_charsets.Charset.ascii,
|
||||
restored.charset.charsets.get(.G1),
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
terminal_ansi.ProtectedMode.dec,
|
||||
restored.protected_mode,
|
||||
);
|
||||
try std.testing.expectEqual(@as(u8, 7), restored.kitty_keyboard.idx);
|
||||
try std.testing.expect(restored.semantic_prompt.seen);
|
||||
try std.testing.expectEqual(
|
||||
TerminalScreen.SemanticPrompt.SemanticClick{
|
||||
.cl = .smart_vertical,
|
||||
},
|
||||
restored.semantic_prompt.click,
|
||||
);
|
||||
|
||||
// Restored page-local cursor IDs are immediately usable by normal writes.
|
||||
try restored.testWriteString("Z");
|
||||
const written = restored.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 7,
|
||||
} }).?;
|
||||
try std.testing.expectEqual(@as(u21, 'Z'), written.cell.codepoint());
|
||||
try std.testing.expectEqual(
|
||||
restored.cursor.style_id,
|
||||
written.cell.style_id,
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
restored.cursor.hyperlink_id,
|
||||
written.node.page().lookupHyperlink(written.cell).?,
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
terminal_page.Cell.SemanticContent.prompt,
|
||||
written.cell.semantic_content,
|
||||
);
|
||||
try std.testing.expectError(error.EndOfStream, restore_source.takeByte());
|
||||
}
|
||||
|
||||
test "SCREEN encodes the minimal complete-page active suffix" {
|
||||
@@ -1761,6 +2192,136 @@ test "SCREEN encodes the minimal complete-page active suffix" {
|
||||
decoded_newest.getRowAndCell(0, 0).cell.codepoint(),
|
||||
);
|
||||
try std.testing.expectError(error.EndOfStream, source.takeByte());
|
||||
|
||||
// Native restoration keeps the incidental history prefix and positions
|
||||
// the active top within the first decoded page.
|
||||
var restore_source: std.Io.Reader = .fixed(destination.written());
|
||||
var restored = try 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.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(u21, 'B'),
|
||||
restored.pages.getTopLeft(.screen).node
|
||||
.page().getRowAndCell(0, 0).cell.codepoint(),
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
@as(u21, 'C'),
|
||||
restored.pages.getTopLeft(.screen).node.next.?
|
||||
.page().getRowAndCell(0, 0).cell.codepoint(),
|
||||
);
|
||||
|
||||
// The restored PageList resumes ordinary scrolling and row growth.
|
||||
try restored.testWriteString("\n");
|
||||
restored.pages.assertIntegrity();
|
||||
restored.assertIntegrity();
|
||||
try std.testing.expectError(error.EndOfStream, restore_source.takeByte());
|
||||
}
|
||||
|
||||
test "SCREEN restoration rejects invalid and incomplete sequences" {
|
||||
var screen = try TerminalScreen.init(
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
.{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 },
|
||||
);
|
||||
defer screen.deinit();
|
||||
|
||||
var destination: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
);
|
||||
defer destination.deinit();
|
||||
try encode(&screen, .primary, &destination);
|
||||
|
||||
// The caller selects which native screen receives the record.
|
||||
{
|
||||
var source: std.Io.Reader = .fixed(destination.written());
|
||||
try std.testing.expectError(
|
||||
error.UnexpectedScreenKey,
|
||||
decode(
|
||||
&source,
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
.alternate,
|
||||
.{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Every truncation must fail without leaking a partially restored
|
||||
// PageList, cursor pin, or cursor-owned state.
|
||||
for (0..destination.written().len) |fixture_len| {
|
||||
var source: std.Io.Reader = .fixed(
|
||||
destination.written()[0..fixture_len],
|
||||
);
|
||||
var restored = decode(
|
||||
&source,
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
.primary,
|
||||
.{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 },
|
||||
) catch continue;
|
||||
restored.deinit();
|
||||
try std.testing.expect(false);
|
||||
}
|
||||
|
||||
// Native PageList construction is transactional on allocation failure.
|
||||
{
|
||||
var failing = std.testing.FailingAllocator.init(
|
||||
std.testing.allocator,
|
||||
.{ .fail_index = 0 },
|
||||
);
|
||||
var source: std.Io.Reader = .fixed(destination.written());
|
||||
try std.testing.expectError(
|
||||
error.OutOfMemory,
|
||||
decode(
|
||||
&source,
|
||||
std.testing.io,
|
||||
failing.allocator(),
|
||||
.primary,
|
||||
.{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// A syntactically valid SCREEN cannot declare an empty PAGE sequence.
|
||||
var empty_sequence: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
);
|
||||
defer empty_sequence.deinit();
|
||||
{
|
||||
var record_writer = try record.Writer.init(
|
||||
&empty_sequence,
|
||||
.screen,
|
||||
);
|
||||
try Header.init(&screen, .primary, 0).encode(
|
||||
record_writer.payloadWriter(),
|
||||
);
|
||||
try record_writer.payloadWriter().writeByte(0);
|
||||
try record_writer.finish();
|
||||
}
|
||||
var empty_source: std.Io.Reader = .fixed(empty_sequence.written());
|
||||
try std.testing.expectError(
|
||||
error.InvalidPageCount,
|
||||
decode(
|
||||
&empty_source,
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
.primary,
|
||||
.{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
test "SCREEN sequence failure preserves preceding bytes" {
|
||||
|
||||
Reference in New Issue
Block a user