terminal/snapshot: accept kitty placeholder cells, track rows

Treat Kitty virtual placeholder codepoints as ordinary valid grid content during snapshot restore and derive the native row lookup hint from decoded cells. Image and placement registries remain intentionally omitted.

Cover the behavior with a complete snapshot round trip containing a real virtual placement and its grapheme diacritics.
This commit is contained in:
Mitchell Hashimoto
2026-07-31 08:16:35 -07:00
parent 66ea61dd9d
commit f8ac0ca98f
3 changed files with 90 additions and 13 deletions

View File

@@ -40,6 +40,9 @@
//!
//! Value 3 is invalid in snapshot version 1.
//!
//! Native row cache flags are not encoded. In particular, the Kitty virtual
//! placeholder hint is derived while decoding cells containing U+10EEEE.
//!
//! ## Cell
//!
//! Each cell has the following format:
@@ -233,9 +236,6 @@ pub const DecodeError = CellHeader.DecodeError || error{
/// The hyperlink map cannot hold the encoded cell references.
InvalidHyperlinkCapacity,
/// PAGE records do not support Kitty graphics placeholders.
UnsupportedKittyGraphics,
};
/// Decode every row and cell directly into an initialized, empty page.
@@ -306,13 +306,18 @@ pub fn decode(
{
return error.InvalidCodepoint;
}
if (cp == kitty.graphics.unicode.placeholder) {
return error.UnsupportedKittyGraphics;
}
if (header.grapheme_count > 0 and cp == 0) {
return error.InvalidGrapheme;
}
cell.content = .{ .codepoint = .{ .data = cp } };
// Kitty image and placement state is not part of this
// snapshot version, but the placeholder is still a valid
// Unicode scalar. Preserve it and derive the native row
// hint so later row operations remain correct.
if (cp == kitty.graphics.unicode.placeholder) {
row.kitty_virtual_placeholder = true;
}
},
.bg_color_palette => {
const palette = header.value.bg_color_palette;
@@ -371,9 +376,6 @@ pub fn decode(
{
return error.InvalidCodepoint;
}
if (suffix == kitty.graphics.unicode.placeholder) {
return error.UnsupportedKittyGraphics;
}
page.appendGrapheme(row, cell, suffix) catch
return error.InvalidGraphemeCapacity;
}

View File

@@ -11,6 +11,7 @@ const record = @import("record.zig");
const screen = @import("screen.zig");
const terminal = @import("terminal.zig");
const Terminal = @import("../Terminal.zig");
const terminal_kitty = @import("../kitty.zig");
const TerminalPageList = @import("../PageList.zig");
const TerminalScreen = @import("../Screen.zig");
const TerminalScreenKey = @import("../ScreenSet.zig").Key;
@@ -363,6 +364,78 @@ test "complete snapshot round trip with history and alternate screen" {
);
}
test "complete snapshot preserves Kitty virtual placeholders" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
var t = try Terminal.init(testing.io, testing.allocator, .{
.cols = 2,
.rows = 1,
});
defer t.deinit(testing.allocator);
// Register a real virtual placement, then write its grid representation:
// U+10EEEE followed by row and column diacritics. The image and placement
// registry is intentionally omitted, but the grid content must remain
// decodable.
try t.screens.active.kitty_images.addImage(
testing.io,
testing.allocator,
.{ .id = 1 },
);
try t.screens.active.kitty_images.addPlacement(
testing.io,
testing.allocator,
1,
0,
.{
.location = .{ .virtual = {} },
.columns = 1,
.rows = 1,
},
);
try t.setAttribute(.{ .@"256_fg" = 1 });
try t.printString("\u{10EEEE}\u{0305}\u{0305}");
const source_cell = t.screens.active.pages.getCell(.{
.screen = .{},
}).?;
try testing.expectEqual(
terminal_kitty.graphics.unicode.placeholder,
source_cell.cell.codepoint(),
);
try testing.expect(source_cell.row.kitty_virtual_placeholder);
var encoded: std.Io.Writer.Allocating = .init(testing.allocator);
defer encoded.deinit();
try encode(&t, &encoded);
var encoded_source: std.Io.Reader = .fixed(encoded.written());
var restored = try decode(
&encoded_source,
testing.io,
testing.allocator,
);
defer restored.deinit(testing.allocator);
const restored_cell = restored.screens.active.pages.getCell(.{
.screen = .{},
}).?;
try testing.expectEqual(
terminal_kitty.graphics.unicode.placeholder,
restored_cell.cell.codepoint(),
);
try testing.expect(restored_cell.cell.hasGrapheme());
try testing.expect(restored_cell.row.kitty_virtual_placeholder);
try testing.expectEqual(
@as(usize, 0),
restored.screens.active.kitty_images.images.count(),
);
try testing.expectEqual(
@as(usize, 0),
restored.screens.active.kitty_images.placements.count(),
);
}
test "complete snapshot encoding is transactional" {
const testing = std.testing;

View File

@@ -228,10 +228,12 @@
//! hover state, and incremental compression state are presentation or cache
//! state and reset during restore.
//!
//! Kitty image state, virtual placeholders, and glyph glossary registrations
//! are unsupported by this snapshot version and are ignored during capture.
//! Build-time terminal behavior, Unicode width policy, parser continuation, and
//! external callbacks are version-level or caller-local requirements.
//! Kitty image state and glyph glossary registrations are unsupported by this
//! snapshot version and are ignored during capture. Unicode virtual placeholder
//! cells are preserved as grid content, but their image and placement state is
//! not restored. Build-time terminal behavior, Unicode width policy, parser
//! continuation, and external callbacks are version-level or caller-local
//! requirements.
//!
//! Native enum declaration indices and mode bit positions used by this format
//! are snapshot-version registries. Changing any of them requires a snapshot