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