diff --git a/src/terminal/snapshot/screen.zig b/src/terminal/snapshot/screen.zig index 20bc24a8e..0b477ee8e 100644 --- a/src/terminal/snapshot/screen.zig +++ b/src/terminal/snapshot/screen.zig @@ -435,7 +435,7 @@ pub fn decode( options.max_scrollback_bytes == 0, .cursor = cursor, .saved_cursor = if (saved_cursor) |value| - value.terminal() + value.terminal(options.cols, options.rows) else null, .charset = header.charset, @@ -813,13 +813,18 @@ pub const SavedCursor = struct { }; } - fn terminal(self: SavedCursor) TerminalScreen.SavedCursor { + fn terminal( + self: SavedCursor, + cols: u16, + rows: u16, + ) TerminalScreen.SavedCursor { + const x = @min(self.x, cols - 1); return .{ - .x = self.x, - .y = self.y, + .x = x, + .y = @min(self.y, rows - 1), .style = self.pen, .protected = self.flags.protected, - .pending_wrap = self.flags.pending_wrap, + .pending_wrap = self.flags.pending_wrap and x == cols - 1, .origin = self.flags.origin, .charset = self.charset, }; @@ -1784,8 +1789,8 @@ test "framed native SCREEN and PAGE sequence" { } 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.expectEqual(@as(u16, 7), restored_saved.x); + try std.testing.expectEqual(@as(u16, 7), 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); @@ -2158,6 +2163,57 @@ test "SCREEN validates pending wrap against a mixed-width cursor page" { decoded.screen.assertIntegrity(); } +test "SCREEN clamps a decoded saved cursor to terminal dimensions" { + 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(); + var stream: record.Writer = .init( + std.testing.allocator, + &destination.writer, + ); + defer stream.deinit(); + + var header = Header.init(&screen, .primary, 1); + header.saved_cursor_present = true; + const screen_payload = stream.begin(.screen); + try header.encode(screen_payload); + try (SavedCursor{ + .x = std.math.maxInt(u16), + .y = std.math.maxInt(u16), + .pen = .{}, + .flags = .{ .pending_wrap = true }, + .charset = .{}, + }).encode(screen_payload); + try screen_payload.writeByte(0); + try stream.finish(); + try page.encode( + screen.pages.getTopLeft(.active).node.pageAssumeResident(), + &stream, + ); + + var source: std.Io.Reader = .fixed(destination.written()); + var decoded = try decode( + &source, + std.testing.io, + std.testing.allocator, + .{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 }, + ); + defer decoded.deinit(); + + const saved = decoded.screen.saved_cursor.?; + try std.testing.expectEqual(@as(u16, 1), saved.x); + try std.testing.expectEqual(@as(u16, 1), saved.y); + try std.testing.expect(saved.pending_wrap); +} + test "SCREEN restoration rejects invalid and incomplete sequences" { var screen = try TerminalScreen.init( std.testing.io,