From f8ac0ca98fcc12c1970560dd40f7bb2e39dc3ba4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 31 Jul 2026 08:16:35 -0700 Subject: [PATCH] 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. --- src/terminal/snapshot/grid.zig | 20 ++++---- src/terminal/snapshot/snapshot.zig | 73 ++++++++++++++++++++++++++++++ src/terminal/snapshot/terminal.zig | 10 ++-- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/src/terminal/snapshot/grid.zig b/src/terminal/snapshot/grid.zig index 4d598aa46..8225cd262 100644 --- a/src/terminal/snapshot/grid.zig +++ b/src/terminal/snapshot/grid.zig @@ -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; } diff --git a/src/terminal/snapshot/snapshot.zig b/src/terminal/snapshot/snapshot.zig index cd7215ab4..90d1c8402 100644 --- a/src/terminal/snapshot/snapshot.zig +++ b/src/terminal/snapshot/snapshot.zig @@ -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; diff --git a/src/terminal/snapshot/terminal.zig b/src/terminal/snapshot/terminal.zig index d0fd0e49f..9bfda575f 100644 --- a/src/terminal/snapshot/terminal.zig +++ b/src/terminal/snapshot/terminal.zig @@ -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